Runtime Abstraction Leaks — CHAOS-1348 Phase 3¶
Copyright © 2026 Morris Richman and the Container-Compose project authors. All rights reserved.
This note records abstraction leaks found while adding MockRuntime, the
second Runtime conformer used to prove the Phase 3 portability boundary from
docs/plans/native-api-server.md. MockRuntime itself has no dependency on
apple/container, apple/containerization, virtualization entitlements, or the
macOS 26 native runtime path.
Leaks discovered during Phase 3 / MockRuntime implementation¶
1. Native runtime skeleton tests exercise apple/containerization-backed types¶
- Location:
Tests/Container-Compose-StaticTests/AppleContainerizationRuntimeTests.swift:17,:43,:44 - Nature: The static test target still includes macOS-only tests that
instantiate
ContainerRegistryandAppleContainerizationRuntime. The file is already gated by#if os(macOS), but it remains integration-shaped coverage for the native backend rather than backend-neutralRuntimecontract coverage. - Proposed fix: Tier 2 integration-test scope. Keep it quarantined behind the existing macOS conditional for now; move it to a dedicated native-runtime test target once the macOS 26 + virtualization entitlement path is fully wired.
2. Runtime extension tests include AppleContainerizationRuntime placeholders¶
- Location:
Tests/Container-Compose-StaticTests/RuntimeProtocolExtensionsTests.swift:75,:80,:91,:95 - Nature: The protocol extension test suite includes two direct
AppleContainerizationRuntimechecks for version metadata and the Phase 3 empty-network placeholder. These are useful backend smoke tests, but they are not portability proof;MockRuntimenow covers the backend-neutral filter semantics in the same file. - Proposed fix: Won't fix immediately. Keep the tests while the native skeleton is still landing; split backend-specific smoke tests out with item 1 when the native-runtime target exists.
3. BridgeContainerClientRuntime is intentionally apple/container-specific¶
- Location:
Sources/Container-Compose/Runtime/BridgeContainerClientRuntime.swift:17,:18 - Nature: The bridge conformer imports
ContainerAPIClientandContainerResource, then translatesContainerSnapshotintoRuntimeContainer. This is a backend adapter, so the imports are expected, but the adapter remains the default runtime inRuntimeEnvironment.current. - Proposed fix: Won't fix — backend-specific by design. Track default backend selection separately; the adapter should remain the apple/container shim until native lifecycle support is deployment-ready.
4. BridgeContainerClientRuntime advertises unsupported protocol members¶
- Location:
Sources/Container-Compose/Runtime/BridgeContainerClientRuntime.swift:77,:99,:106,:122,:129,:143,:150,:157 - Nature:
RuntimeError.notSupportedis thrown for bridge-only gaps:listNetworks,wait, and secret CRUD. Earlier Phase 1 bridge gaps forcreate,start,kill,logs,events, andstatisticsnow delegate throughContainerClientProvider. Remaining gaps are adapter limitations, not protocol gaps;MockRuntimeimplements the full surface in memory. - Proposed fix: Incrementally shrink this list as commands migrate from
apple/container CLI/XPC paths to a native runtime. Keep
notSupportedvisible so API routes can return explicit 501/adapter-gap responses instead of silently fabricating data.
5. LogRingBuffer imports Containerization for production writer plumbing¶
- Location:
Sources/Container-Compose/Runtime/LogRingBuffer.swift:17 - Nature: The reusable log buffer imports
Containerizationbecause the native backend's stdout/stderr writer path is tied to apple/containerization writer APIs.MockRuntimebypasses this by storingRuntimeLogFramevalues directly in test memory. - Proposed fix: Consider extracting a backend-neutral ring buffer from the
writer-facing adapter if log replay moves into more non-native conformers.
Not required for CHAOS-1348 because the public
Runtime.logssurface remainsRuntimeLogFrame-only.
Leaks discovered during Phase 4 / CHAOS-1358 (stats backend)¶
6. BridgeContainerClientRuntime aggregates network stats into a single eth0 entry¶
- Location:
Sources/Container-Compose/Runtime/BridgeContainerClientRuntime.swift—translate(stats:)method added in CHAOS-1358 - Nature:
ContainerStats(fromContainerResource) provides only aggregate network receive and transmit byte totals (networkRxBytes,networkTxBytes), not per-interface breakdown. The bridge translates these into a single syntheticRuntimeStatistics.Networkentry withinterface: "eth0". Real multi-interface containers will see their network traffic rolled up into one entry with a misleading interface name. - Proposed fix: Wire per-interface stats from the vsock path once
AppleContainerizationRuntimehas full lifecycle support.ContainerStatsmay gain a per-interface breakdown in a futureapple/containerrelease; updatetranslate(stats:)when that happens.
7. AppleContainerizationRuntime.statistics returns empty snapshot (no vsock call)¶
- Location:
Sources/Container-Compose/Runtime/AppleContainerizationRuntime.swift—statistics(for:)method - Nature: The native conformer cannot call
LinuxContainer.statistics()because Phase 1's skeleton does not hold a[String: LinuxContainer]map — the registry storesRuntimeContainerRecordvalues, not liveLinuxContainerinstances. Thestatistics(for:)implementation confirms the container exists in the registry and returns an empty snapshot (all CPU/memory fieldsnil). Clients streaming/containers/{id}/statsagainst the Apple runtime see structurally valid NDJSON frames with null metric fields. - Proposed fix: When Phase 2 lifecycle wiring adds real
LinuxContainerinstances, extendAppleContainerizationRuntimewith acontainers: [String: LinuxContainer]or similar map, then callcontainer.statistics()in this method. TheContainerStatistics→RuntimeStatisticstranslation should mapcpu.usageUsec,memory.usageBytes,memory.limitBytes,memoryEvents.oomKill, andnetworks[].receivedBytes/transmittedBytesdirectly.
8. ✅ RESOLVED — BridgeContainerClientRuntime.statistics no longer translates all errors as notFound¶
- Location:
Sources/Container-Compose/Runtime/BridgeContainerClientRuntime.swift—statistics(for:)error catch block - Nature:
ContainerClient.stats(id:)can throw various upstream errors (XPC timeout, auth failure, container not found). The catch block translates all errors uniformly toRuntimeError.notFound(id:)to avoid leakingContainerizationErrortypes across the abstraction boundary. This means an XPC timeout is indistinguishable from a missing container at the route layer — the client receives 404 rather than 503. - Resolution: Fixed in
b18a9ce.BridgeContainerClientRuntime.statistics(for:)now inspects typedContainerizationErrorvalues, preserves not-found semantics, and maps all other upstream failures toRuntimeError.backendFailure(message:).StatsRoutesnow returns 502 for backend failures while keeping genuine missing-container cases on 404.
Leaks discovered during Phase 8 / CHAOS-1353 (resource CRUD)¶
9. BridgeContainerClientRuntime and AppleContainerizationRuntime both throw .notSupported for all network CRUD operations¶
- Location:
Sources/Container-Compose/Runtime/BridgeContainerClientRuntime.swift—createNetwork(spec:)andremoveNetwork(id:)extensionSources/Container-Compose/Runtime/AppleContainerizationRuntime.swift— same extension- Nature: Neither the
apple/containerXPC client (ContainerAPIClient) nor theapple/containerizationSwift package exposes a public Swift API for programmatic network creation or deletion. ThecontainerCLI handles network management via shell invocations to platform networking tools;container-composecurrently replicates this in Compose command paths (e.g.compose upcallscontainer network createviaRunCommandRunner). Because theRuntimeprotocol is the boundary for API-server operations and neither backend supports these operations, both conformers return.notSupported. - Proposed fix: When
apple/containerizationadds network management APIs (tracked upstream), wireAppleContainerizationRuntimefirst. The bridge conformer can follow when the XPC surface grows. ThePOST /networksandDELETE /networks/{id}routes already translate.notSupportedto HTTP 501.
10. AppleContainerizationRuntime local volume CRUD still delegates through apple/container's volume registry¶
- Location:
Sources/Container-Compose/Runtime/RuntimeVolumeClient.swiftSources/Container-Compose/Runtime/BridgeContainerClientRuntime.swift— volume CRUD extensionSources/Container-Compose/Runtime/AppleContainerizationRuntime.swift— same extension- Nature: CHAOS-1368 closed the functional gap for local named volumes by routing
listVolumes/createVolume/removeVolumethroughClientVolume(apple/container's XPC-backed volume registry). That makes theRuntimesurface usable for Compose named volumes and REST volume routes. The remaining abstraction leak is architectural:AppleContainerizationRuntimestill cannot manage volumes throughapple/containerizationalone, so its volume CRUD path depends on the apple/container service instead of the native containerization package. - Proposed fix: When
apple/containerizationgrows a public named-volume API, replaceRuntimeVolumeClientwith a truly native backend forAppleContainerizationRuntime. Keep the bridge backend onClientVolumeunless and until the bridge itself is retired.
11. BridgeContainerClientRuntime and AppleContainerizationRuntime both throw .notSupported for all secret CRUD operations¶
- Location:
Sources/Container-Compose/Runtime/BridgeContainerClientRuntime.swift— secret CRUD extensionSources/Container-Compose/Runtime/AppleContainerizationRuntime.swift— same extension- Nature: Neither
apple/containerizationnorapple/container's XPC surface has a concept of secrets. Phase 8 ships in-memory secret storage only viaMockRuntime; a durable backend (macOS Keychain, encrypted file store, or a dedicated secrets daemon) is deferred. Cross-host secret distribution is explicitly out of scope for CHAOS-1353. - Proposed fix: Implement
AppleContainerizationRuntime.createSecret/listSecrets/removeSecretbacked by the macOS Keychain (Security.framework,SecItemAdd,SecItemCopyMatching,SecItemDelete) for single-host use. When container-compose gains multi-host support (Phase N), bridge to a dedicated secrets manager (e.g. HashiCorp Vault, AWS Secrets Manager) via a pluggable backend interface.
Leaks discovered during Phase 5 / CHAOS-1354 (lifecycle write endpoints)¶
12. BridgeContainerClientRuntime.start does not distinguish container-not-found from other errors¶
- Location:
Sources/Container-Compose/Runtime/BridgeContainerClientRuntime.swift—start(id:)implementation added in CHAOS-1354 - Nature:
ContainerClientProvider.start(id:)callsContainerClient.bootstrap(id:stdio:)+process.start(). If the container does not exist, the XPC call throwsContainerizationError(.notFound), but the catch block wraps all errors asRuntimeError.backendFailure(message:)to avoid leakingContainerizationErrortypes across the abstraction boundary. The route layer therefore returns 500 instead of 404 for a missing container when using the Bridge backend. - Proposed fix: Inspect the upstream error code and map
ContainerizationError(.notFound)toRuntimeError.notFound(id:)before wrapping everything else asbackendFailure. This matches the established pattern inBridgeContainerClientRuntime.get(id:).
Leaks discovered during Phase 6 / CHAOS-1352 (POST /containers/create)¶
13. ✅ RESOLVED — BridgeContainerClientRuntime.create no longer throws .notSupported¶
- Location:
Sources/Container-Compose/Runtime/BridgeContainerClientRuntime.swift—create(id:configuration:);Sources/Container-Compose/Runtime/ContainerClientProvider.swift—create(id:configuration:) - Resolution: Fixed in CHAOS-1365.
BridgeContainerClientRuntime.create()delegates toContainerClientProvider.create(id:configuration:). The production provider reuses apple/container'sUtility.containerConfigFromFlags(...)helper to fetch/unpack the image, resolve the default kernel viaClientKernel, map process/resource/publish/capability fields, callContainerClient.create(configuration:options:kernel:initImage:), then read back the created snapshot. - Remaining caveat: The bridge path still depends on the apple/container XPC daemon and image/kernel resolution side effects. Upstream create failures are mapped through
RuntimeErrorMapper, so duplicate containers can surface asalreadyExistsand other XPC failures surface asbackendFailurerather than a route-level 501.
Leaks discovered during Phase 7 / CHAOS-1360 (project lifecycle endpoints)¶
14. ProjectOrchestrator.build and .pull emit synthetic frames — no Runtime.build/pull protocol method¶
- Location:
Sources/Container-Compose/Server/ProjectOrchestrator.swift—buildStream(...)andpullStream(...)static methods - Nature:
POST /projects/{name}/buildandPOST /projects/{name}/pullare supposed to emit real build/pull progress. TheProjectOrchestratoruses only the existingRuntimeprotocol methods (list,get,create,start,stop,remove). There is noRuntime.build(image:options:)orRuntime.pull(image:options:)protocol method. ThebuildStreamandpullStreammethods emit "not supported via daemon API" frames instead of actual build or pull output. Real image build/pull requires theRunCommandRunnerseam (a CLI-level concern), which is not accessible from theRuntimeprotocol layer. - Impact:
POST /projects/{name}/buildandPOST /projects/{name}/pullalways return "notSupported" frames regardless of runtime backend. The NDJSON stream shape is correct, so clients can parse it, but no actual build or pull is triggered. - Proposed fix: Add
build(service:context:options:) -> AsyncStream<BuildFrame>andpull(imageReference:) -> AsyncStream<PullFrame>to theRuntimeprotocol.BridgeContainerClientRuntimecan delegate toRunCommandRunner.run(.swiftAPI(name: "BuildCommand"), ...)andRunCommandRunner.run(.swiftAPI(name: "ImagePull"), ...).MockRuntimecan emit synthetic frames for testing. This decouples project lifecycle routes from CLI internals while using the existing runner seam architecture.
Leaks discovered during Phase 9 / CHAOS-1359 (TCP/TLS transport)¶
15. ListenAddress shape leaks into idempotence probe — TCPProbe is transport-aware¶
- Location:
Sources/Container-Compose/Commands/TCPProbe.swift—canConnect(to:port:timeoutSeconds:)andServeDaemon.isAlreadyServing(listenAddress:)inComposeServe.swift - Nature:
ServeDaemon.isAlreadyServing(listenAddress:)branches on the concreteListenAddresscase to choose betweenUnixSocketProbe(for.unix) andTCPProbe(for.tcp/.tls). This means the daemon startup code is now explicitly aware of the transport layer type at the idempotence-check site. In a cleaner abstraction, the probe logic would be fully encapsulated behindListenAddress.probe()or aListenAddressProberprotocol, hiding the branch from the call site. - Impact: Low. The branch is already co-located with
ListenAddresssemantics and both probe implementations are small (< 50 lines each). The leak does not cross theRuntimeprotocol boundary and does not affect testability. - Proposed fix: Introduce
ListenAddress.isAlreadyBound() -> Boolto encapsulate the dispatch behind the enum itself. Not required for CHAOS-1359 — save for a future "daemon client cleanup" PR.