| Item | Value |
|---|---|
| Status | Integrated design complete; implementation not started |
| Family | container-engine-api, container-engine, container, containerization, container-compose, container-builder-shim, devcontainer, and supporting matched-stack projects |
| Compatibility target | Docker Compose 5.3.1 with Docker Engine 29.2.1 API 1.53 on macOS |
| Matched Container revision | 88460ab2ab0ca2f3fa9f91b2911b3b77647596c1 |
| Matched Containerization revision | d7377b962af724f8d7c2b640f3ab12184d33f1af |
| Original devcontainer design evidence | b31e80b2b9c09ecc73bb3badf9cd5cf16550a538 on origin/main; extraction head must be reselected from a clean reviewed revision |
| Socktainer comparison | 1.1.1 at 6cc7a32cc37d4ad0c07e9c88a7bbf2abdaceeea0; conformance input only |
| Design date | 31 July 2026 |
The parity gaps cannot be closed as independent adapters. They share identity, lifecycle, storage, networking, namespaces, cgroups, devices, security profiles, logging, and one Docker API authority. The coherent implementation has four foundations:
- One runtime-neutral Engine gateway. A per-user
container-engineservice, built from a sharedcontainer-engine-apipackage extracted from devcontainer, owns the Docker-compatible Unix listener, wire protocol, route ledger, connection safety, provider selection, and protocol projection. - Exactly one selected runtime authority. The gateway routes to one immutable provider fingerprint and one associated state root. Enhanced mode uses the matched Container authority; standalone devcontainer mode uses a stock-Apple adapter. There is no automatic fallback, federation, second listener, or simultaneous writer.
- One enhanced Linux engine sandbox. Complete Docker namespace, volume, network, cgroup, device, security, logging-plugin, and privileged semantics run in one per-user Linux kernel with private workload namespaces and cgroups by default. Containerization's experimental
LinuxPodis productionised intoEngineLinuxSandbox; one VM per container is not the parity topology. - One durable workload transaction model. Container owns immutable identity, lifecycle state/events, and an operation ledger that composes durable policies and typed leases from specialised network, volume/mount, rootfs-storage, workload resource/cgroup, device, security-profile/ID-map, logging, socket, namespace, and model-route controllers. Containerization applies effective Linux plans and does not interpret Docker or Compose policy.
devcontainer is a first-class Container-family project even though it remains capable of running on stock Apple components. Its hardened Docker socket implementation becomes the shared gateway rather than a competing daemon. Socktainer is treated as an important Apple-container ecosystem interoperability and socket-relay reference, not as a second authority or a substitute for the API 1.53 parity target.
This document is the dependency and authority design for:
- advanced network and IPAM;
- non-local volumes, advanced mounts, and API socket;
- Docker logging-driver semantics;
- remaining resource and security controls;
- Docker lifecycle states and actions;
- model-runner services;
- Local Deploy device/resource subset; and
- shared namespaces and Docker-complete privileged isolation.
It resolves cross-document conflicts and orders implementation. Each focused design remains normative for its protocol/behavioural details; this document is normative where topology, authority, ownership, dependency, migration, or shared primitives cross those boundaries.
Implementation, review convergence, local-first validation, main checkpoints, MBP runner use, upstream surveillance, and parity/performance acceptance follow the Container-family parity development cycle.
- Product implementation in this change; this is design only.
- Running two providers against one socket/state root or merging their live state automatically.
- Routing requests silently to Docker Desktop, Colima, Socktainer, or another daemon when the selected provider is unavailable.
- Making devcontainer import ComposeCore or require the matched Container forks in its stock provider.
- Reimplementing Docker Model Runner, Docker logging plugins, network/volume provider protocols, or Linux kernel facilities in Compose.
- Claiming Swarm scheduling, Windows containers, or arbitrary macOS hardware where Docker Compose local mode or host APIs cannot provide it.
These invariants apply to every focused design:
- One user-visible Engine socket has one owning process, one selected provider fingerprint, and one state root.
- One immutable container ID identifies the same resource through native Container, Compose, Docker HTTP/CLI, and devcontainer clients; name is separate and mutable.
- The selected runtime provider is the only resource/lifecycle/event writer. The Engine gateway may persist protocol/provider selection and Docker-only compatibility data only through the provider's transaction contract.
- Enhanced native workloads run in one
EngineLinuxSandbox; they are not divided among feature-specific or per-container VMs. - Every workload has private PID/IPC/network/UTS/cgroup/mount namespaces, cgroup, rootfs, endpoint, and policy by default. Supported PID/IPC/network donor sharing is explicit and resolves to an immutable donor ID; supported host modes select the sandbox's corresponding namespace. Mount namespaces and rootfs remain per-workload under every namespace mode, while bind propagation remains a mount-plane policy. User-namespace mappings follow the selected Engine ID-map policy, with
userns=hostas the explicit exemption. - VM capacity is distinct from workload cgroup policy. A service CPU or memory limit never resizes the shared VM.
- Requested, resolved/effective, ignored-with-warning, and observed state remain distinct.
- Compose/YAML strings end at typed policies. Containerization receives only provider-resolved Linux primitives.
- Network, volume/mount, rootfs-storage, device, profile, socket, logging, namespace, and model-route allocations are durable typed leases coordinated by one workload-ledger transaction, not uncoordinated side effects. This is one authority decision over frozen controller receipts, not a claim of one physical transaction across controller stores. Resource/cgroup intent is a durable requested/effective policy revision; only its live cgroup materialisation is process/sandbox-generation-bound.
- Privileged and Engine-socket access are independent. Privilege never implies a socket grant; a socket grant is full Engine authority unless Docker itself says otherwise.
- Host-native services, sandbox service workloads, and user containers have separate lifecycle/logging/health models.
- A capability mismatch fails before unsafe side effects; no layer silently degrades to metadata-only success.
- Behavioural parity and performance are proven separately against pinned references.
| Conflict or hidden dependency | Resolution |
|---|---|
| Network design assumed per-workload VZ/vmnet attachment, while namespace sharing requires arbitrary same-kernel network donors. | vmnet becomes VM-level multiplexed backhaul. Guest networking creates dynamic per-workload network namespaces and veth/TAP endpoints; a joiner owns none and delegates to its donor. |
| Volume design made a shared Linux storage/workload authority conditional on a managed-directory oracle. | The EngineLinuxSandbox is foundational for enhanced parity regardless of fast-path results. Oracle-qualified managed-directory mounts may remain optimisations inside the same authority model. |
| Logging design proposed a Linux provider plane started only for journald/plugins. | Logging providers run as protected service workloads/namespaces in the same EngineLinuxSandbox; no second provider VM exists. Native file writers may remain a macOS-side fast path. |
| Existing API-socket design made Container itself the listener, which would make standalone devcontainer depend on Stephen's forks. | Split the neutral Engine listener/router from runtime ownership. container-engine loads exactly one stock or enhanced provider across a process/XPC boundary. |
| Devcontainer and the enhanced family pin stock and forked Apple packages with the same SwiftPM identities. | container-engine-api imports neither Apple package. Stock/enhanced adapters remain out of process and communicate through a versioned provider protocol. |
| Lifecycle, Compose, Engine, and devcontainer could each manufacture state/events. | The selected provider owns one immutable identity store, lifecycle controller, and canonical event journal. Clients filter/project it; they do not synthesise missing transitions. |
| Namespace donors rely on names and can be recreated. | Resolve names/service references to immutable IDs before create; Compose cascade-recreates dependants when donor recreation changes the ID. Leases are generation-bound. |
| Resource, Deploy, privilege, volumes, and models each need devices/accelerators. | One DeviceBroker owns guest device inventory, providers, CDI edits, and leases. Host Model Runner uses a distinct host-accelerator admission/lease rather than pretending Metal is a guest GPU. |
privileged was treated as a stronger isolation mode. |
It intentionally grants Docker-equivalent authority over the Engine Linux host/siblings. The outer security boundary is the macOS authority/VM. It still grants no Engine socket or macOS credential implicitly. |
| Local Deploy gap description called for CPU/PID/generic reservations and generic/device limits. | Match the schema and local projection exactly: CPU and generic-resource reservations are preserved/ignored; reservation PIDs and limit devices/generic resources are schema-invalid; CPU/memory/PID limits and memory reservation use ordinary controls; every reservation DeviceRequest, including non-GPU, uses DeviceBroker. The job-specific wait/restart gates are already verified removed; only the remaining false rejection work is in scope. |
| Security profiles could be read later from mutable host paths. | Compose resolves project-relative seccomp paths and snapshots compact inline content/digest before create. The profile controller owns validated immutable content. |
Rootfs storage_opt, block devices, volume attachments, and device requests could allocate the same host artefact independently. |
The workload ledger composes rootfs, volume, and DeviceBroker leases and rejects conflicts before materialisation. |
| Model runner could be represented as a provider service/container or supervised by the neutral gateway. | Use the pinned host-native Docker Model Runner under the enhanced Container authority's supervision and state root. container-engine only routes the selected-provider model SPI. Workloads see an inference-only stable guest route. |
| Socket relays could be treated as a safe project-limited pseudo-engine. | Match Docker: /var/run/docker.sock conveys broad/full Engine authority. Relay ownership is explicit, auditable, revocable, and independent of namespace/privilege. |
| The devcontainer evidence revision predates substantial local router/runtime work, while the sibling checkout is dirty. | Treat the old revision as original evidence only. Select a clean, reviewed, immutable extraction head before implementation; never extract from or rewrite the dirty checkout. |
flowchart TB
subgraph Clients["Clients"]
Compose["container-compose"]
Native["Container CLI/native XPC"]
Dev["devcontainer"]
Docker["Docker CLI and API clients"]
end
Compose --> Gateway
Dev --> Gateway
Docker --> Gateway
Native --> Enhanced
Gateway["one per-user container-engine Unix gateway"] --> Select["exclusive provider selection and fingerprint"]
Select --> Stock["stock-Apple devcontainer provider"]
Select --> Enhanced["matched Container authority provider"]
Enhanced --> Identity["identity, lifecycle, events, workload ledger"]
Identity --> Resolver["typed WorkloadPlanResolver"]
Resolver --> Network["network and IPAM controller"]
Resolver --> Volume["volume and mount controller"]
Resolver --> Rootfs["rootfs storage controller"]
Resolver --> Resource["workload resource and cgroup controller"]
Resolver --> Device["DeviceBroker"]
Resolver --> Security["profile and ID-map controller"]
Resolver --> Logging["logging controller"]
Resolver --> Socket["Engine-socket grant controller"]
Resolver --> Namespace["namespace dependency controller"]
Resolver --> ModelRoute["workload model-route controller"]
Resolver --> Sandbox["EngineLinuxSandbox manager"]
Sandbox --> Linux["one Linux kernel; private namespaces/cgroups per workload"]
Linux --> ProviderSvc["protected logging/plugin services"]
Enhanced --> Model["host-native Docker Model Runner controller"]
Model --> ModelRoute
ModelRoute --> Inference["inference-only sandbox gateway route"]
| Component | Owns | Must not own |
|---|---|---|
container-engine-api |
Docker wire DTOs/streams, versioned router metadata, Unix server primitives, neutral runtime/provider SPI, capability/fingerprint types, conformance tests. | Compose policy, devcontainer policy, Apple runtime packages, resources, provider state, credentials. |
container-engine |
One Unix listener, peer/path safety, protocol/version projection, route capability ledger, selected-provider session, connection/backpressure limits, socket discovery. | A second copy of provider resource truth, model supervision/store/credentials, or automatic fallback/federation. |
| enhanced Container authority | Canonical resources, immutable identity/name indexes, lifecycle/events, workload ledger/controllers, sandbox and host-service supervision. | Parsing Compose YAML, Docker wire handling, provider-specific code in Containerization. |
| stock devcontainer provider | Standalone adapter over exact upstream Apple tags plus any stock-lane compatibility state required by the neutral SPI. | Imports of Stephen's fork packages, shared enhanced state claims, private listener when container-engine already owns it. |
containerization |
Generic Linux VM/sandbox, namespace, cgroup, process, rootfs/mount, device, network and guest-agent primitives. | Docker/Compose semantics, provider registry, lifecycle events, stable resource identity. |
container-compose |
Compose model/selection/hash/reconciliation/progress/policy and typed requests. | Runtime resources, Docker Engine listener, inference, log transports, device or storage allocation. |
container-builder-shim |
Matched Container build/BuildKit adaptation and build-session integration through the shared Engine contract. | A second Engine listener, runtime inventory, or unversioned /session shortcut. |
devcontainer |
Dev Container specification/workflows and provider selection; packaging of stock provider where required. | A duplicate Docker router/socket/database after migration; ComposeCore dependency. |
| Docker Model Runner | Pinned upstream inference and model APIs/backend. | Container lifecycle/resource authority, Compose selection, guest management exposure. |
Create a dedicated runtime-neutral package/repository, provisionally stephenlclarke/container-engine-api, by extracting reviewed devcontainer implementation with history. It provides:
ContainerEngineWire: Engine version parsing, request/response/error types, JSON/JSONL, raw/multiplexed/hijacked streams, archive transfer, and field-preserving envelopes generated from the pinned Engine API;ContainerEngineRouter: versioned route matching/dispatch and a maintained per-route capability/compatibility ledger;ContainerUnixHTTPServer: hardened NIO Unix listener, bounded/spooled bodies, connection limits, backpressure, pipelining, half-close/upgrades, singleton lock, peer/owner checks, and inode-safe cleanup; andContainerEngineRuntimeSPI: neutral async provider operations, streams, capabilities, identity fingerprint, and transactional mutation claims/protected-effect references.
The existing devcontainer router is valuable implementation and test evidence but is not assumed complete. Known gaps such as missing routes, registry-auth handling, and unknown-field loss are closed against the generated API 1.53 ledger. Existing tests move with extracted code and pass before either consumer changes.
The audited sibling checkout is dirty and contains substantial work beyond the original origin/main evidence revision. Implementation MUST first resolve that branch independently, choose a clean immutable accepted head, and document the extraction diff. This design does not modify or bless the dirty checkout.
A provider fingerprint includes provider kind, implementation semantic version,
Apple/fork runtime build revisions, protocol capability set, state schema, and
the immutable state-root UUID created at initialisation. It explicitly excludes
process IDs, boot IDs, live sandbox identity, sandboxGeneration, guest-agent
session, and current capability/inventory observations; those are ephemeral
reconciliation tokens and a normal restart must not look like a provider
switch. Fingerprints and deterministic legacy-ID migration use the state-root
UUID rather than serialising a host path. container-engine persists the
selected fingerprint and refuses a genuinely different provider/state root for
the same socket while resources or an incomplete handoff exist.
The handoff control records are themselves versioned durable state:
public enum ProviderHandoffPartDispositionV1: String, Codable, Sendable {
case included
case empty
case unsupported
case retainOffline
case explicitResolutionRequired
}
public enum ProviderHandoffPartKindV1: String, Codable, Sendable {
case identityLifecycleEvents
case imagesAndContent
case networksAndIPAM
case volumesAndMounts
case rootfsConfigsAndSecrets
case socketGrants
case resourcesSecurityProfilesAndIDMaps
case devices
case namespaces
case logging
case modelsAndRoutes
case buildsAndCache
}
public enum ProviderHandoffCanonicalEncodingV1: String, Codable, Sendable {
case deterministicCBORV1
}
public enum ProviderHandoffDigestAlgorithmV1: String, Codable, Sendable {
case sha256
case lineageHMACSHA256V1
case orderedLineageHMACSHA256AggregateV1
}
public enum ProviderHandoffPayloadProtectionV1: String, Codable, Sendable {
case authenticatedPlaintext
case destinationSealedX25519HKDFSHA256XChaCha20Poly1305V1
}
public enum ProviderHandoffContentDigestScopeV1: String, Codable, Sendable {
case publicSHA256V1
case singleSourceLineageHMACSHA256V1
case multiSourceLineageHMACSHA256AggregateV1
}
public struct ProviderHandoffContentSourceDigestV1: Codable, Sendable {
public var sourceStateRootUUID: String
public var authorityLineageUUID: String
public var lineageDigestKeyVersion: UInt64
public var orderedEntryIDs: [String]
public var sourceDigestHMACSHA256: String
}
public struct ProviderHandoffCanonicalContentDigestV1: Codable, Sendable {
public var algorithm: ProviderHandoffDigestAlgorithmV1
public var scope: ProviderHandoffContentDigestScopeV1
public var orderedSourceDigests: [ProviderHandoffContentSourceDigestV1]
public var digest: String
}
public struct ProviderHandoffPayloadPackageEntryV1: Codable, Sendable {
public var entryID: String
public var sourceStateRootUUID: String?
public var recordKind: String
public var schemaVersion: UInt32
public var canonicalRecordBytes: Data
}
public struct ProviderHandoffPayloadPackageV1: Codable, Sendable {
public var schemaVersion: UInt32
public var partKind: ProviderHandoffPartKindV1
public var entries: [ProviderHandoffPayloadPackageEntryV1]
}
public struct ProviderHandoffPayloadDescriptorV1: Codable, Sendable {
public var bundleObjectID: String
public var mediaType: String
public var schemaVersion: UInt32
public var canonicalEncoding: ProviderHandoffCanonicalEncodingV1
public var canonicalPlaintextByteLength: UInt64
public var transportByteLength: UInt64
public var canonicalContentDigest: ProviderHandoffCanonicalContentDigestV1
public var transportDigestSHA256: String
public var protection: ProviderHandoffPayloadProtectionV1
public var destinationEncryption: ProviderHandoffPayloadEncryptionV1?
}
public struct ProviderHandoffPartV1: Codable, Sendable {
public var kind: ProviderHandoffPartKindV1
public var schemaVersion: UInt32
public var disposition: ProviderHandoffPartDispositionV1
public var sourceStateRootUUIDs: [String]
public var requiredCapabilities: [String]
public var payload: ProviderHandoffPayloadDescriptorV1
}
public enum ProviderHandoffSignatureAlgorithmV1: String, Codable, Sendable {
case ed25519V1
}
public enum ProviderHandoffPublicKeyAlgorithmV1: String, Codable, Sendable {
case ed25519V1
case x25519V1
}
public enum ProviderHandoffKeyRoleV1: String, Codable, Sendable {
case gatewayCoordinator
case sourceProvider
case destinationProvider
}
public enum ProviderHandoffKeyPurposeV1: String, Codable, Sendable {
case trustRegistrySigning
case sourceManifestSigning
case coordinatorManifestSigning
case lineageKeyEnvelopeSigning
case coordinatorCommitSigning
case coordinatorTerminalOutcomeSigning
case destinationPossessionSigning
case destinationPayloadEncryption
case destinationLineageKeyEncryption
}
public struct ProviderHandoffPublicKeyProvenanceV1: Codable, Sendable {
public var enrollmentID: String
public var owningBundleIdentifier: String
public var codeRequirementDigestSHA256: String
public var teamIdentifier: String?
public var providerRegistrationDigestSHA256: String
public var enrolledAtUnixSeconds: UInt64
public var enrollmentProofSignature: Data?
}
public struct ProviderHandoffTrustKeyV1: Codable, Sendable {
public var keyID: String
public var algorithm: ProviderHandoffPublicKeyAlgorithmV1
public var role: ProviderHandoffKeyRoleV1
public var purpose: ProviderHandoffKeyPurposeV1
public var providerFingerprint: String?
public var stateRootUUID: String?
public var rawPublicKey: Data
public var provenance: ProviderHandoffPublicKeyProvenanceV1
public var notBeforeUnixSeconds: UInt64
public var notAfterUnixSeconds: UInt64
public var rotationPredecessorKeyID: String?
public var revokedAtUnixSeconds: UInt64?
public var revocationReason: String?
}
public struct ProviderHandoffSignatureV1: Codable, Sendable {
public var algorithm: ProviderHandoffSignatureAlgorithmV1
public var purpose: ProviderHandoffKeyPurposeV1
public var signerKeyID: String
public var signerRole: ProviderHandoffKeyRoleV1
public var providerFingerprint: String?
public var stateRootUUID: String?
public var trustRegistryRevision: UInt64
public var canonicalBytesVersion: UInt32
public var signedProjectionDigestSHA256: String
public var signature: Data
}
public struct ProviderHandoffTrustRegistryV1: Codable, Sendable {
public var schemaVersion: UInt32
public var registryRevision: UInt64
public var issuedAtUnixSeconds: UInt64
public var keys: [ProviderHandoffTrustKeyV1]
public var registryDigestSHA256: String
public var registrySignature: ProviderHandoffSignatureV1
}
public enum ProviderHandoffEnvelopeAlgorithmV1: String, Codable, Sendable {
case x25519HKDFSHA256XChaCha20Poly1305V1
}
public enum ProviderHandoffAEADObjectKindV1: String, Codable, Sendable {
case partPayload
case lineageKeyEnvelope
case destinationPossessionChallenge
}
public struct ProviderHandoffAEADAssociatedDataV1: Codable, Sendable {
public var schemaVersion: UInt32
public var objectKind: ProviderHandoffAEADObjectKindV1
public var tokenID: String
public var manifestID: String
public var objectLocalID: String
public var partKind: ProviderHandoffPartKindV1?
public var mediaType: String?
public var payloadSchemaVersion: UInt32?
public var canonicalPlaintextByteLength: UInt64
public var canonicalContentDigest: ProviderHandoffCanonicalContentDigestV1?
public var sourceStateRootUUID: String?
public var authorityLineageUUID: String?
public var lineageDigestKeyVersion: UInt64?
public var destinationProviderFingerprint: String
public var destinationStateRootUUID: String
public var destinationKeyPurpose: ProviderHandoffKeyPurposeV1
public var destinationKeyID: String
public var ephemeralPublicKey: Data
public var nonce: Data
}
public struct ProviderHandoffPayloadEncryptionV1: Codable, Sendable {
public var encryptionAlgorithm: ProviderHandoffEnvelopeAlgorithmV1
public var destinationKeyPurpose: ProviderHandoffKeyPurposeV1
public var destinationKeyID: String
public var ephemeralPublicKey: Data
public var nonce: Data
public var associatedDataDigestSHA256: String
}
public enum ProviderHandoffRootRoleV1: String, Codable, Sendable {
case source
case destination
}
public struct ProviderHandoffControllerRevisionV1: Codable, Sendable {
public var controllerID: String
public var revision: UInt64
public var canonicalStateDigestSHA256: String
}
public struct ProviderHandoffRevisionVectorV1: Codable, Sendable {
public var schemaVersion: UInt32
public var stateRootUUID: String
public var rootStoreRevision: UInt64
public var snapshotCheckpointID: String?
public var controllerRevisions: [ProviderHandoffControllerRevisionV1]
public var revisionVectorDigestSHA256: String
}
public struct ProviderHandoffHeaderExpectationV1: Codable, Sendable {
public var schemaVersion: UInt32
public var role: ProviderHandoffRootRoleV1
public var stateRootUUID: String
public var expectedHeader: StateRootHeaderV1
public var expectedHeaderDigestSHA256: String
public var preCommitRevisionVector: ProviderHandoffRevisionVectorV1
public var abortHeader: StateRootHeaderV1
public var abortHeaderDigestSHA256: String
public var abortRevisionVector: ProviderHandoffRevisionVectorV1
}
public struct ProviderHandoffSourceV1: Codable, Sendable {
public var providerFingerprint: String
public var stateRootUUID: String
public var authorityLineageUUID: String
public var lineageDigestKeyVersion: UInt64
public var preCommitExpectation: ProviderHandoffHeaderExpectationV1
public var sourceSignature: ProviderHandoffSignatureV1
}
public struct DestinationSealedLineageKeyEnvelopeV1: Codable, Sendable {
public var envelopeID: String
public var sourceStateRootUUID: String?
public var authorityLineageUUID: String
public var keyVersion: UInt64
public var destinationKeyPurpose: ProviderHandoffKeyPurposeV1
public var destinationKeyID: String
public var encryptionAlgorithm: ProviderHandoffEnvelopeAlgorithmV1
public var ephemeralPublicKey: Data
public var nonce: Data
public var canonicalPlaintextByteLength: UInt64
public var associatedDataDigestSHA256: String
public var ciphertext: Data
public var envelopeSignature: ProviderHandoffSignatureV1
}
public struct ProviderHandoffDestinationKeyPossessionProofV1: Codable, Sendable {
public var schemaVersion: UInt32
public var proofID: String
public var tokenID: String
public var manifestID: String
public var destinationProviderFingerprint: String
public var destinationStateRootUUID: String
public var destinationKeyPurpose: ProviderHandoffKeyPurposeV1
public var destinationKeyID: String
public var challengeEphemeralPublicKey: Data
public var challengeNonce: Data
public var challengeAssociatedDataDigestSHA256: String
public var challengeCiphertext: Data
public var responseDigestSHA256: String
public var destinationSignature: ProviderHandoffSignatureV1
}
public struct ProviderHandoffManifestV1: Codable, Sendable {
public var schemaVersion: UInt32
public var manifestID: String
public var tokenID: String
public var trustRegistryRevision: UInt64
public var destinationKeyPossessionProofDigestsSHA256: [String]
public var sources: [ProviderHandoffSourceV1]
public var resultingAuthorityLineageUUID: String
public var resultingLineageDigestKeyVersion: UInt64
public var destinationSealedLineageKeyEnvelopes: [DestinationSealedLineageKeyEnvelopeV1]
public var destinationProviderFingerprint: String
public var destinationStateRootUUID: String
public var destinationPreCommitExpectation: ProviderHandoffHeaderExpectationV1
public var parts: [ProviderHandoffPartV1]
public var manifestDigestAlgorithm: ProviderHandoffDigestAlgorithmV1
public var manifestDigest: String
public var coordinatorSignature: ProviderHandoffSignatureV1
}
public enum ProviderHandoffPhaseV1: String, Codable, Sendable {
case draining
case quiesced
case staged
case aborting
case committed
case reconciling
case complete
case aborted
}
public struct ProviderHandoffProviderSelectionRecordV1: Codable, Sendable {
public var schemaVersion: UInt32
public var selectionRevision: UInt64
public var selectedProviderFingerprint: String?
public var selectedStateRootUUID: String?
public var providerRegistrationDigestSHA256: String?
public var trustRegistryRevision: UInt64
}
public struct ProviderHandoffProviderSelectionExpectationV1: Codable, Sendable {
public var expectedRecord: ProviderHandoffProviderSelectionRecordV1
public var expectedRecordDigestSHA256: String
public var resultingRecord: ProviderHandoffProviderSelectionRecordV1
public var resultingRecordDigestSHA256: String
}
public struct ProviderHandoffSocketDiscoveryRecordV1: Codable, Sendable {
public var schemaVersion: UInt32
public var discoveryRevision: UInt64
public var socketInstanceUUID: String
public var ownerUID: UInt32
public var minimumEngineAPIVersion: String
public var maximumEngineAPIVersion: String
public var selectedProviderFingerprint: String?
public var selectedStateRootUUID: String?
}
public struct ProviderHandoffSocketSelectionExpectationV1: Codable, Sendable {
public var expectedRecord: ProviderHandoffSocketDiscoveryRecordV1
public var expectedRecordDigestSHA256: String
public var resultingRecord: ProviderHandoffSocketDiscoveryRecordV1
public var resultingRecordDigestSHA256: String
}
public struct ProviderHandoffTokenV1: Codable, Sendable {
public var schemaVersion: UInt32
public var tokenID: String
public var tokenRevision: UInt64
public var orderedSourceStateRootUUIDs: [String]
public var destinationProviderFingerprint: String
public var destinationStateRootUUID: String
public var trustRegistryRevision: UInt64
public var resultingAuthorityLineageUUID: String
public var resultingLineageDigestKeyVersion: UInt64
public var phase: ProviderHandoffPhaseV1
public var preCommitRootExpectations: [ProviderHandoffHeaderExpectationV1]
public var destinationKeyPossessionProofDigestsSHA256: [String]
public var manifestID: String
public var manifestDigest: String?
public var importedParts: [ProviderHandoffPartImportExpectationV1]?
public var authoritativeCommitRevision: UInt64?
public var commitDigestSHA256: String?
public var handoffChainHeadDigestSHA256: String?
public var rootPrepareRecordDigestsSHA256: [String]?
public var terminalOutcomeDigestSHA256: String?
}
public enum ProviderHandoffPartStagingStateV1: String, Codable, Sendable {
case declared
case retrieving
case transportVerified
case decrypted
case contentVerified
case imported
case compensationRequired
case compensated
}
public enum ProviderHandoffPartStagingFailureClassV1: String, Codable, Sendable {
case transport
case authentication
case canonicalContent
case capability
case collision
case importEffect
case compensation
}
public struct ProviderHandoffByteRangeV1: Codable, Sendable {
public var lowerBound: UInt64
public var upperBoundExclusive: UInt64
}
public struct ProviderHandoffSourceDigestVerificationV1: Codable, Sendable {
public var sourceStateRootUUID: String
public var authorityLineageUUID: String
public var lineageDigestKeyVersion: UInt64
public var computedSourceDigestHMACSHA256: String
}
public struct ProviderHandoffPartStagingRecordV1: Codable, Sendable {
public var schemaVersion: UInt32
public var tokenID: String
public var manifestID: String
public var manifestDigest: String
public var partKind: ProviderHandoffPartKindV1
public var bundleObjectID: String
public var payloadDescriptorDigestSHA256: String
public var stagingRevision: UInt64
public var state: ProviderHandoffPartStagingStateV1
public var receivedRanges: [ProviderHandoffByteRangeV1]
public var verifiedTransportDigestSHA256: String?
public var sourceDigestVerifications: [ProviderHandoffSourceDigestVerificationV1]
public var verifiedCanonicalContentDigest: String?
public var stagedImportReceiptDigestSHA256: String?
public var lastFailureClass: ProviderHandoffPartStagingFailureClassV1?
}
public struct ProviderHandoffPartImportExpectationV1: Codable, Sendable {
public var partKind: ProviderHandoffPartKindV1
public var payloadDescriptorDigestSHA256: String
public var stagedImportReceiptDigestSHA256: String
}
public struct ProviderHandoffRootPrepareRecordV1: Codable, Sendable {
public var schemaVersion: UInt32
public var tokenID: String
public var manifestID: String
public var role: ProviderHandoffRootRoleV1
public var stateRootUUID: String
public var commitDigestSHA256: String
public var expectedHeaderDigestSHA256: String
public var preCommitRevisionVectorDigestSHA256: String
public var postCommitHeaderDigestSHA256: String
public var postCommitRevisionVectorDigestSHA256: String
public var prepareRevision: UInt64
}
public struct ProviderHandoffCommitIntentV1: Codable, Sendable {
public var schemaVersion: UInt32
public var tokenID: String
public var manifestID: String
public var manifestDigest: String
public var trustRegistryRevision: UInt64
public var authoritativeCommitRevision: UInt64
public var preCommitRootExpectations: [ProviderHandoffHeaderExpectationV1]
public var importedParts: [ProviderHandoffPartImportExpectationV1]
public var destinationKeyPossessionProofDigestsSHA256: [String]
public var providerSelection: ProviderHandoffProviderSelectionExpectationV1
public var socketSelection: ProviderHandoffSocketSelectionExpectationV1
public var resultingAuthorityLineageUUID: String
public var resultingLineageDigestKeyVersion: UInt64
public var resultingMinimumWriterSchemaVersion: UInt32
}
public struct ProviderHandoffPostCommitRootV1: Codable, Sendable {
public var role: ProviderHandoffRootRoleV1
public var stateRootUUID: String
public var postCommitHeader: StateRootHeaderV1
public var postCommitHeaderDigestSHA256: String
public var postCommitRevisionVector: ProviderHandoffRevisionVectorV1
}
public struct ProviderHandoffCommitRecordV1: Codable, Sendable {
public var schemaVersion: UInt32
public var intent: ProviderHandoffCommitIntentV1
public var commitDigestSHA256: String
public var handoffChainHeadDigestSHA256: String
public var postCommitRoots: [ProviderHandoffPostCommitRootV1]
public var rootPrepareRecordDigestsSHA256: [String]
public var coordinatorSignature: ProviderHandoffSignatureV1
}
public struct ProviderHandoffTerminalRootV1: Codable, Sendable {
public var role: ProviderHandoffRootRoleV1
public var stateRootUUID: String
public var terminalHeader: StateRootHeaderV1
public var terminalHeaderDigestSHA256: String
public var terminalRevisionVector: ProviderHandoffRevisionVectorV1
}
public struct ProviderHandoffTerminalOutcomeV1: Codable, Sendable {
public var schemaVersion: UInt32
public var tokenID: String
public var manifestID: String
public var manifestDigest: String?
public var phase: ProviderHandoffPhaseV1
public var roots: [ProviderHandoffTerminalRootV1]
public var outcomeDigestSHA256: String
public var coordinatorSignature: ProviderHandoffSignatureV1
}The manifest is the authenticated immutable index of one handoff bundle. Every
part kind occurs once and has exactly one ProviderHandoffPayloadDescriptorV1
and therefore one immutable payload object. bundleObjectID is
sha256:<lowercase-hex transportDigestSHA256> after the transport bytes exist;
it is never a caller path, URL, mutable upload ID, or an input to encryption.
Every payload object is a deterministic-CBOR
ProviderHandoffPayloadPackageV1 containing an ordered index and any number of
bounded immutable entries. An entry is inline in that object and has no
bundleObjectID, transport digest, URL, or independently replaceable body.
Entry IDs are non-empty, unique within the part, and entries are ordered by the
manifest source order and then the encoded UTF-8 bytes of entryID; public
coordinator-only disposition entries follow all source entries. Every part,
including empty, unsupported, or explicitResolutionRequired, carries at
least one structured evidence entry. There is no zero-length implicit part.
Deterministic CBOR v1 is the sole signature/digest encoding: definite lengths,
shortest integer forms, text already validated as NFC UTF-8, map keys ordered
first by encoded length and then bytewise lexicographically, array order
preserved, and no floats, duplicate map keys, tags, indefinite values, or
implementation-object encoding. Encoding never silently normalises caller text;
non-NFC text is rejected by the owning schema before projection. Unless a rule
below says HMAC, a
domain digest is the raw 32 bytes of
SHA-256(ASCII(domain) || 0x00 || deterministicCBOR(projection)) and is rendered
as lowercase hex only at the wire boundary. UUIDs use their lowercase canonical
text in CBOR; SHA/HMAC values inside a projection are 32-byte byte strings, not
their hex text.
The following projections are exact:
ProviderHandoffRevisionVectorV1excludes onlyrevisionVectorDigestSHA256. Controller IDs are unique and ordered by their encoded bytes. Its domain iscontainer-handoff-revision-vector-v1. At the pre-commit boundary a source has a non-nil checkpoint and a complete entry for every controller; the data-pristine destination has a nil checkpoint and an empty controller array because imports are in the separate staging store. Post-commit/terminal vectors use the same schema with their complete current checkpoint/controller set.rootStoreRevisionis always present and participates in compare-and-swap.StateRootHeaderV1has no embedded digest. Its complete deterministic-CBOR value, including the prior chain head and writer epoch, uses domaincontainer-state-root-header-v1. AProviderHandoffHeaderExpectationV1is valid only when the expected and abort headers, root UUID, both revision-vector root UUIDs, role, and all recomputed digests agree. Its expected header is the already-fencedsourceQuiesced/destinationStagedvalue. Its abort header is the exact alternative that clears the token/staged lineage, restores the source's recorded pre-drain writable state or the destination's priornonestate, retains the prior chain/provider/lineage fields appropriate to that role, and incrementswriterEpochonce. The abort revision vector increments onlyrootStoreRevisiononce and retains the checkpoint and every ordered controller revision/digest from the pre-commit vector.- Complete
ProviderHandoffProviderSelectionRecordV1andProviderHandoffSocketDiscoveryRecordV1values use domainscontainer-handoff-provider-selection-v1andcontainer-handoff-socket-discovery-v1. Each expectation embeds both records and their recomputed digests; a digest-only or partially reconstructed selection is invalid. - The unsigned manifest excludes only
manifestDigestandcoordinatorSignature; its domain iscontainer-handoff-manifest-v1and its algorithm is always.sha256. It includes complete source signatures, complete signed key envelopes, destination proof digests, and every final payload descriptor. Its destination expectation must have role.destinationand match the token and later commit intent byte-for-byte. A source-signature projection contains the common manifest/token/trust/destination/resulting-lineage fields, exactly thatProviderHandoffSourceV1withsourceSignatureomitted, its signed lineage-key envelopes, and each contributed part descriptor plus that source's digest entry when the part is sealed (the complete public descriptor otherwise); it contains no source-signature value from any source. Its domain iscontainer-handoff-source-manifest-v1. The coordinator signs the resulting manifest digest and destination/root/token binding under.coordinatorManifestSigning.
Canonical content verification is independent of a source's assertion:
- An authenticated-public payload has algorithm
.sha256, scope.publicSHA256V1, an emptyorderedSourceDigests, and digestSHA-256("container-handoff-public-content-v1" || 0x00 || deterministicCBOR(complete ProviderHandoffPayloadPackageV1)). - Every entry in a sealed payload has a non-nil source root and belongs to
exactly one source partition. A source partition contains that source's
entries in package order and uses
HMAC-SHA-256(lineageKey, ASCII("container-handoff-source-content-v1") || 0x00 || deterministicCBOR(sourceProjection)).sourceProjectionis a closed map with exactly the named fieldstokenID,manifestID,packageSchemaVersion,partKind,sourceStateRootUUID,authorityLineageUUID,lineageDigestKeyVersion, andorderedCompleteEntries; the last field contains the complete entry maps in package order, not just IDs or record-byte digests. The descriptor records the source/root/key-version tuple, ordered entry IDs, and resulting HMAC. No entry may occur in two partitions, and every sealed package entry must occur in one. - A one-partition payload has algorithm
.lineageHMACSHA256V1, scope.singleSourceLineageHMACSHA256V1, exactly that one complete value inorderedSourceDigests, and a top-level digest equal to that partition's raw HMAC. A payload with two or more partitions has algorithm.orderedLineageHMACSHA256AggregateV1and scope.multiSourceLineageHMACSHA256AggregateV1;orderedSourceDigestshas exactly two or more unique partitions in manifest source order and its digest isSHA-256("container-handoff-multi-source-content-v1" || 0x00 || deterministicCBOR(array of ordered complete ProviderHandoffContentSourceDigestV1 values)).
Protection and content scope are paired: .authenticatedPlaintext is valid only
with .sha256/.publicSHA256V1, while
.destinationSealedX25519HKDFSHA256XChaCha20Poly1305V1 is valid only with one of
the lineage-HMAC algorithm/scope pairs above. For authenticated plaintext,
destinationEncryption is nil and the transport bytes are exactly the canonical
package bytes. For a destination-sealed descriptor, destinationEncryption is
non-nil, its purpose is exactly
.destinationPayloadEncryption, and the transport bytes are exactly the AEAD
ciphertext followed by its tag. In both cases canonicalPlaintextByteLength is
the canonical package-byte count, while transportByteLength and
transportDigestSHA256 cover only the resulting transport bytes. Compression,
padding, an outer archive, or another transport transform is not permitted in
v1. The package schemaVersion and partKind must equal the descriptor and
manifest part before any entry is accepted.
A package containing any sensitive record is sealed as one object; all of its records, including associated metadata, are source-partitioned. Public and lineage-HMAC scopes cannot be mixed in one descriptor. The destination verifies each signed lineage-key envelope, opens it only in its protected staging store, recomputes every source HMAC from the decrypted canonical package, checks the ordered aggregate, and then wipes or rewraps the raw key. A missing envelope, wrong source/key version, duplicate/unassigned entry, or digest mismatch fails before import or commit. This makes multi-secret and multi-source content verifiable without publishing a plaintext SHA-256 fingerprint.
For every part, sourceStateRootUUIDs is the unique manifest-source-ordered set
of roots named by its package entries. For a sealed part it must also equal the
roots in orderedSourceDigests; a source with no entry is absent from both. The
array is provenance, not permission to omit or merge a partition.
While the token is held, the authenticated bundle service retrieves an object
only by (tokenID, manifestID, bundleObjectID), enforces
transportByteLength before allocation, streams it resumably into a private
staging store, verifies the transport digest before decrypt/decode, and verifies
the canonical content rules above after decode. Objects remain available
through reconciliation or abort and are collected only after terminal-retention
rules permit it.
The current-user gateway owns a Keychain-backed, signed
ProviderHandoffTrustRegistryV1. Its bootstrap registry-signing Ed25519 public
key is installed with the gateway and pinned by key ID and code requirement;
the registry cannot authorise its own replacement. Each registry key has one
algorithm, role, purpose, provider/root binding, validity interval, rotation
predecessor, revocation state, and registry-signed provenance. Provider keys
require both providerFingerprint and stateRootUUID; gateway-coordinator keys
require both nil. A fingerprint alone is never a verification or encryption
key.
Trust keys are unique and ordered by encoded keyID. The unsigned registry
projection excludes registryDigestSHA256 and registrySignature; its domain is
container-handoff-trust-registry-v1. The bootstrap key signs that digest using
purpose .trustRegistrySigning and the generic signature projection below. A
duplicate key ID, a recomputed key ID mismatch, an overlap that gives two active
keys the same role/purpose/provider/root without an explicit predecessor chain,
or a registry revision that is not exactly the previous revision plus one is
invalid.
keyID is the lowercase hex domain digest
container-handoff-trust-key-id-v1 over algorithm, role, purpose,
provider/root binding, and the raw public key. Ed25519 public keys and signatures
are exactly 32 and 64 bytes. An Ed25519 enrollment proof is a self-signature over
the key-ID projection plus the provenance record with
enrollmentProofSignature omitted under domain
container-handoff-ed25519-enrollment-v1; it is present and exactly 64 bytes for
every non-bootstrap Ed25519 key. It is nil for the separately pinned bootstrap
key and every X25519 key. Registry acceptance attests an X25519 key's provenance
but never proves possession; the token-scoped challenge below is mandatory
before either X25519 purpose is used. The registry signature then attests the
accepted provenance. Every operational signature names one purpose and its
registry revision. Its signedProjectionDigestSHA256 is the appropriate domain
digest above. The Ed25519 message bytes are exactly
ASCII("container-handoff-signature-v1") || 0x00 || deterministicCBOR(signatureProjection),
where signatureProjection is the closed map of purpose, signerRole,
algorithm, signerKeyID, providerFingerprint, stateRootUUID,
trustRegistryRevision, canonicalBytesVersion, and the raw 32-byte
signedProjectionDigestSHA256. This is ordinary Ed25519, not Ed25519ph, and no
extra hash, JSON encoding or signature field is admitted. Algorithm, purpose,
role, provider/root, key ID, raw key, and projection must all match the same
trust entry.
| Purpose | Required algorithm, role, and binding |
|---|---|
.trustRegistrySigning |
Ed25519 bootstrap gatewayCoordinator; provider/root nil. |
.coordinatorManifestSigning, .coordinatorCommitSigning, .coordinatorTerminalOutcomeSigning |
Ed25519 gatewayCoordinator; provider/root nil. |
.sourceManifestSigning |
Ed25519 sourceProvider; exact source provider fingerprint and root. |
.lineageKeyEnvelopeSigning |
Ed25519 sourceProvider bound to the transferred source, or gatewayCoordinator with nil bindings only for a newly created resulting-lineage key. |
.destinationPossessionSigning |
Ed25519 destinationProvider; exact destination provider fingerprint and root. |
.destinationPayloadEncryption, .destinationLineageKeyEncryption |
X25519 destinationProvider; exact destination provider fingerprint and root. |
A key authorises new work only when the token issue/use time is inside its inclusive validity interval and no signed revocation effective at or before the use exists. Rotation adds a separately proven key with a new ID and optional predecessor; new tokens use the newest valid key, while an already-bound token may finish with its still-valid, unrevoked key. Revocation fences every pre-commit token at its next phase transition. The destination retains each old X25519 private key in its protected Keychain until every token bound to that key is complete or aborted and every associated staged payload, opened lineage key, and envelope has been wiped; deleting only the public registry entry or rotating early must not strand recovery. Archived signed registry revisions and old public keys remain read-only for terminal-record verification through the retention window, but cannot authorise replay or new work.
A pre-commit token fenced by expiry or revocation may perform only signed abort, receipt-driven compensation, protected-store wipe, and terminal evidence retention. Cleanup operates from staging receipts and object ownership and never marks an unopened envelope decrypted/content-verified or invents a successful HMAC. A revocation observed after the signed commit decision cannot restore a source or erase that decision; recovery consumes the already verified imports and receipts, and fences for explicit intervention if those committed inputs are unrecoverable rather than using the revoked key to authorise new decryption.
X25519 static and ephemeral public keys are raw 32-byte little-endian
u-coordinates. Decoding requires the high bit clear, the integer strictly less
than 2^255 - 19, and a value other than zero or one. The implementation also
rejects any input whose constant-time X25519 operation produces the all-zero
32-byte shared secret; this rejects the remaining low-order inputs. Private keys
are CSPRNG-generated and clamped by the X25519 implementation. Each payload,
key envelope, and possession challenge uses a fresh ephemeral key and a fresh
24-byte XChaCha20 nonce; an ephemeral-public-key/nonce pair may never repeat for
one destination key.
For every sealed object, compute the one
ProviderHandoffAEADAssociatedDataV1 projection first. Fields not applicable to
its tagged objectKind are nil; applicable fields must be present. It includes
the ephemeral public key and nonce but deliberately excludes
associatedDataDigestSHA256, ciphertext/tag, transport length/digest,
bundleObjectID, manifest digest/signatures, commit digest, chain head, and all
post-commit headers. Its raw associated-data digest is the domain digest
container-handoff-aead-associated-data-v1, and those 32 digest bytes are the
only XChaCha20-Poly1305 additional authenticated data.
objectLocalID is fixed before encryption: it is part:<partKind-raw-value> for
a part payload, the already-random envelopeID for a lineage-key envelope, and
the already-random proofID for a possession challenge. It is unique within
that token/manifest/object kind. The later content-addressed bundleObjectID,
transport digest, or any value derived from ciphertext is prohibited in
objectLocalID, the associated-data projection, HKDF salt, or HKDF info.
Associated-data applicability is closed, not implementation-selected:
| Object kind | Required object fields | Required content fields | Fields that must be nil |
|---|---|---|---|
.partPayload |
objectLocalID = part:<kind>, descriptor part kind, media type, schema version and plaintext byte length. |
Complete canonical content digest. | Source root, lineage UUID and lineage key version. |
.lineageKeyEnvelope |
objectLocalID = envelopeID, fixed media type application/vnd.io.github.stephenlclarke.container.handoff-lineage-key.v1+cbor, payload schema version 1 and the envelope's plaintext byte length. |
Envelope source root (nil only for a newly created resulting lineage), lineage UUID and key version. | Part kind and canonical content digest. |
.destinationPossessionChallenge |
objectLocalID = proofID and plaintext byte length 32. |
No optional content field. | Part kind, media type, payload schema version, canonical content digest, source root, lineage UUID and lineage key version. |
Every row also requires schema version 1, token/manifest IDs, destination
provider/root, destination key purpose/ID, ephemeral public key, and nonce. The
duplicated metadata in a payload descriptor, key envelope, or proof record must
be byte-equal to the associated-data projection. Payload and lineage-envelope
purposes are respectively .destinationPayloadEncryption and
.destinationLineageKeyEncryption; a challenge uses the exact purpose it is
proving. A nil, extra, or mismatched field is an authentication failure rather
than a default.
Given the non-zero 32-byte X25519 shared secret, derive exactly 32 AEAD-key bytes
with RFC 5869 HKDF-SHA-256, using the raw shared secret as IKM. The salt is the
raw 32-byte domain digest container-handoff-hkdf-salt-v1 over a closed map with
exactly the named fields tokenID, manifestID, objectKind, objectLocalID,
destinationProviderFingerprint, destinationStateRootUUID,
destinationKeyPurpose, and destinationKeyID, copied byte-for-byte from the
associated-data projection. The info byte string is
ASCII("container-handoff-x25519-xchacha20poly1305-key-v1") || 0x00 || associatedDataDigestBytes; HKDF output length is 32 with no secondary split or
expansion. AEAD is the XChaCha20-Poly1305-IETF construction: HChaCha20 consumes
nonce bytes 0 through 15, and ChaCha20-Poly1305-IETF consumes
0x00000000 || nonce[16...23]. Its AAD is exactly the raw 32-byte associated-data
digest and its plaintext is the exact unpadded, uncompressed bytes declared for
the object. Encryption emits ciphertext followed by the 16-byte Poly1305 tag;
that combined value is the payload object's transport bytes or the
envelope/challenge ciphertext field. Transport digest and content-addressed
object ID are computed only afterwards, so neither encryption nor the manifest
contains a digest cycle.
Payloads use a trust key whose sole purpose is
.destinationPayloadEncryption; lineage-key envelopes use
.destinationLineageKeyEncryption. An envelope plaintext is the
deterministic-CBOR closed map with exactly schemaVersion = 1,
sourceStateRootUUID, authorityLineageUUID, keyVersion, and one
rawLineageHMACSHA256Key 32-byte byte string. Its source may be nil only for a
newly created resulting-lineage key; all other plaintext bindings must equal the
outer envelope and associated data before the key is admitted. The envelope is
covered, including ciphertext, plaintext byte length and AEAD metadata, by a
valid .lineageKeyEnvelopeSigning signature over domain
container-handoff-lineage-key-envelope-v1 with only envelopeSignature
omitted, and is verified before opening. A newly created resulting-lineage
envelope is signed by the coordinator; a transferred key is signed by its source
provider. The envelope's signer role/provider/root must match that choice.
Before any payload or lineage-key envelope is encrypted to a destination X25519
key, the gateway performs a token/manifest-scoped proof of possession for each
selected encryption purpose.
It encrypts a random 32-byte challenge through the same AEAD/KDF contract with
object kind .destinationPossessionChallenge. The destination decrypts it and
returns the domain digest container-handoff-destination-key-proof-v1 over a
closed map containing every persisted proof field from schemaVersion through
challengeCiphertext, plus challengePlaintext as the exact decrypted 32-byte
byte string; responseDigestSHA256 and destinationSignature are excluded. The
completed proof-record digest uses domain
container-handoff-destination-key-proof-record-v1 over all persisted fields
except destinationSignature, including that response digest. The destination's
separately authorised .destinationPossessionSigning Ed25519 key signs the
proof-record digest. The gateway compares the response in constant time,
discards the plaintext challenge, and binds the completed proof-record digest
into the token, manifest, and commit intent. Proof digests are unique and ordered
by encryption-purpose raw value and then encoded destination key ID, with exactly
one for each key/purpose used by any payload or envelope. A signature alone,
provider claim, or successful X25519 public-key parse is not proof of private-key
possession.
The signed manifest bytes never change after the token first binds its digest.
Retrieval and import progress lives only in one token/manifest/part-scoped
ProviderHandoffPartStagingRecordV1. Its immutable identity fields must match
the signed descriptor; only stagingRevision, state, normalised non-overlapping
received ranges, verification results, opaque import-receipt digest, and bounded
failure class may change. Every update compare-and-swaps stagingRevision.
Progress loss replays retrieval from the immutable object; progress from another
token, manifest digest, part, or descriptor is never adopted.
The staging store and any controller-private tentative effects are a distinct
token-owned namespace outside public indexes and the authoritative root/controller
revision vectors. A staged import can change only its staging record and protected
receipt; it cannot advance rootStoreRevision, a controller revision, or a
canonical-state digest. Promotion after the signed decision performs ordinary
controller transactions and advances those revisions during reconciliation.
Abort compensation removes the tentative effects and proves their absence before
the unchanged pre-commit vector can take its one header-only abort transition.
The state order is declared, retrieving, transport verified, decrypted, content
verified, and imported. A public payload skips decrypted; a sealed payload
cannot. An import with externally staged effects records one opaque,
controller-verifiable receipt before imported. Abort moves an effected record
through compensationRequired -> compensated; failure remains fenced and
resumable. No progress field, receipt, collision decision, or extracted entry can
rewrite a payload descriptor, manifest digest, package entry, or source
signature. Commit requires exactly one imported record for every required part
and no unresolved or compensation-required record. Once that condition holds,
one token CAS records the manifest-ordered descriptor/receipt expectation array
and advances quiesced -> staged. That CAS freezes the imported records: they
may thereafter be read for prepare/commit or, after a proved absent decision,
and a successful token CAS to aborting, moved through compensation, but not
otherwise revised or replaced. The destination root's
prepare operation re-verifies every frozen descriptor/receipt expectation
against the commit intent. Any drift rejects prepare rather than modifying the
manifest or pretending the gateway can atomically CAS a controller store.
Token IDs and manifest IDs are cryptographically random and single-use. The
token freezes a non-empty unique ordered source-root array, one distinct
destination provider/root, trust-registry revision, resulting lineage/key
version, and manifest ID before drain. The gateway later binds one manifest
digest and destination-proof set before bundle reads and rejects the same token
with any other value. Root expectations use exactly the frozen source order
followed by that destination. Every token mutation compare-and-swaps and increments
tokenRevision once. A committed, aborted, or expired token cannot authorise
another handoff; recovery may resume only its already-bound manifest and phase.
Abort first compare-and-swaps any pre-decision draining, quiesced, or
staged token to aborting; the commit decision accepts only staged, so a
prepare-release/abort and a commit cannot both win. Signed envelopes may be
opened for verification during protected staging after these bindings and all
pre-commit expectations/signatures pass; opening an envelope does not transfer
authority, and abort wipes or compensates its staged key.
Token transitions are closed:
draining -> quiesced -> staged -> committed -> reconciling -> complete, or
draining|quiesced|staged -> aborting -> aborted. aborting, committed,
reconciling, complete, and aborted reject every transition not shown;
especially, no state at or after committed can abort and no aborting token can
prepare or commit.
ProviderHandoffCommitIntentV1 is frozen only after every part is imported. Its
root expectations are unique and ordered as manifest sources followed by the
destination. It contains the assigned monotonically increasing gateway commit
revision, complete pre-commit headers and revision vectors, manifest/trust/key
proof bindings, the manifest-ordered imported-part descriptor/receipt
expectations, exact before/after provider-selection record, exact before/after
socket-discovery record, resulting lineage/key version, and writer floor. The
provider and socket result revisions must each be exactly their expected revision
plus one. Provider selection changes only its selected registration/root/trust
binding. Socket discovery retains its socket instance, owner, and API range and
changes only revision plus selected provider/root. Both resulting records select
the manifest destination fingerprint/root and its manifest-bound trust revision,
after the gateway has checked the latest registry for intervening revocation.
The commit sequence is deliberately one-way:
- Compute
commitDigestSHA256as the domain digestcontainer-handoff-commit-intent-v1over the complete commit intent. It contains only pre-commit headers and has no chain head or post-commit header. - Compute
handoffChainHeadDigestSHA256as the domain digestcontainer-handoff-chain-head-v1over the commit digest and the ordered(stateRootUUID, prior handoffChainHeadDigest)values copied from the pre-commit headers. - Deterministically derive each post-commit header with that chain head. Sources
become
sourceTransferred, clear the active token, incrementwriterEpoch, and retain their provider/lineage. The destination adopts the resulting lineage/key version and provider, clears staged lineage, becomesdestinationReconciling, retains the active token, raises the writer floor, and increments its epoch. Compute each post-header digest withcontainer-state-root-header-v1. Derive its post-commit revision vector by incrementing onlyrootStoreRevisiononce and retaining the checkpoint and every ordered controller revision/digest from the pre-commit vector. - Through the authenticated provider session, ask each root to atomically check
its exact pre-commit header/revision vector and token-fenced handoff lock, then
persist one immutable
ProviderHandoffRootPrepareRecordV1in that lock's separate metadata store. Its closed complete projection uses domaincontainer-handoff-root-prepare-v1, binds the commit digest and exact pre/post header/vector digests, and increments that lock'sprepareRevision; it does not change either authoritative revision vector. Roots are prepared in manifest source order followed by the destination; after each response the gateway verifies or recovers that exact immutable record and CAS-appends its digest to the token's ordered prepare array. A partial array is resumable but cannot commit. - Assemble
ProviderHandoffCommitRecordV1with the complete post-root values and ordered prepare-record digests. Sign its projection excluding onlycoordinatorSignature, under purpose.coordinatorCommitSigningand domaincontainer-handoff-commit-record-v1.
Thus the commit digest determines the chain head, which determines post-commit headers/vectors and prepare records, which determine the signed commit record; no digest depends back on a post-header, prepare record, chain head, signature, or ciphertext that depends on it.
The coordinator decision transaction compare-and-swaps the token from staged,
its manifest binding, next commit revision, current trust/proof set, exact
provider/socket selection expectations, every imported staging descriptor/
receipt expectation frozen in that token, and the ordered set of still-current
immutable root-prepare record digests. A prepare lock rejects abort, another
prepare, authoritative
controller mutation, or header change until the gateway first CASes the token
from its pre-decision phase to aborting and explicitly releases it. Therefore
the gateway does
not pretend to execute a cross-store compare-and-swap: each root performs its
own local exact CAS before the one gateway decision. If any gateway value or
root prepare differs, it writes no commit record or selection result and leaves
sources authoritative and quiesced for resume or the mutually exclusive
aborting prepare-release path. If
they all match, it atomically persists the signed commit record, resulting
provider/socket selections, and token phase committed in the gateway authority
store. This signed record is the authority decision even when separate physical
root-header writes have not all completed.
After the decision, each prepared root uses an idempotent two-value CAS: the exact pre-commit header/vector plus matching prepare record writes the exact derived post-header/vector; the exact post pair plus matching applied prepare record succeeds without another epoch or revision increment; any third value fences the root and requires explicit recovery. Applying or releasing a prepare updates only the separate handoff-lock metadata. A crash with no valid commit record can only resume staging/prepare or release every prepare and abort to the signed post-abort pairs. A crash with a valid commit record can never release a prepare, abort, or restore a source writer: recovery verifies the signature/digests, repairs all missing post pairs and selections, then continues destination reconciliation. A record whose intent, chain, prepare, post pair, selection, or physical root cannot be verified keeps every writer fenced; process presence and staged files are never authority evidence.
Once every prepared post pair and both selection records match the signed
decision, the gateway CASes committed -> reconciling; only that phase may
promote the frozen destination imports through controller transactions. Complete
requires every promoted controller revision/receipt and source tombstone to be
reconciled, then atomically records the signed Complete outcome and token phase
complete. No reconciliation failure reopens the commit choice.
Abort and Complete each produce one canonical terminal-outcome projection. Its
unsigned deterministic-CBOR form excludes the digest/signature; its SHA-256 and
coordinator signature use the terminal domain separator
container-handoff-outcome-v1 and purpose
.coordinatorTerminalOutcomeSigning. The gateway terminal transaction binds
that outcome digest to the token. Each terminal root embeds its complete header,
header digest and revision vector; recovery compare-and-swaps or verifies that
exact pair. Once pre-commit expectations exist, an aborted outcome uses their
frozen abortHeader/abortRevisionVector, already source/coordinator-signed when
a manifest exists, never the pre-handoff expected values. The terminal outcome
always signs the resulting pairs. An earlier draining abort uses each root's
token-bound local handoff journal to CAS its exact current pair back to the
recorded prior authority state; an unrecorded or mismatched journal remains
fenced. A Complete outcome records the
actual post-reconciliation source pairs and the
destination's single header transition from destinationReconciling to
destinationActive; that transition clears the token, increments writer epoch
and root-store revision once, and leaves controller revisions at their already
reconciled values. phase in a terminal outcome is valid only as aborted or
complete, root UUIDs are unique and in manifest source order followed by the
destination (or the token's frozen source order when an early abort has no
manifest), and a missing/mismatched root keeps recovery fenced.
The manifest has a closed required part inventory. Every focused kind appears exactly once, including when it is empty, unsupported, retained only as offline evidence, or requires explicit resolution. A required unsupported/unresolved part prevents commit. Part payload descriptors cover their canonical bytes, transport representation, disposition and contributing roots; the manifest digest covers the ordered source set, destination, lineage, key envelopes and all parts. Every source signature binds its typed pre-commit header/revision-vector expectation, lineage, signed key envelopes, and contributed source partitions; the coordinator signature binds the complete manifest. Recovery follows only the signed commit/terminal records and exact compare-and-swap rules above, never which processes or staged files exist.
Part ownership is exact even when one focused controller document contributes to several parts:
| Part | Sole canonical owner |
|---|---|
identityLifecycleEvents |
Immutable container/name/alias records, lifecycle/restart state, canonical event/audit disposition, the generic operation ledger, every generic idempotency/retry/outcome/tombstone record, and pending/completed lifecycle finalisers. |
imagesAndContent |
OCI image/index/manifest/config/layer descriptors, platform/tag references, content-store ownership and verified export/re-pull disposition; never Model Runner content or build-cache records. |
networksAndIPAM |
Network/IPAM resources, complete effective allocator state, endpoints/DNS/port intent and provider provenance; never live namespace/forwarding handles. |
volumesAndMounts |
Named-volume resources/provider provenance, population state, mount intent and durable attachment leases; no rootfs backing or generated artefact content. |
rootfsConfigsAndSecrets |
Rootfs-storage leases/backing/content export or re-materialisation disposition, config/secret/generated-artifact metadata, sealed content and protected digests. |
socketGrants |
Inbound/Engine socket grant intent, revocation/audit state and references to artifact IDs; it never duplicates artifact metadata or bytes. |
resourcesSecurityProfilesAndIDMaps |
Requested/effective cgroup policy, security-profile records/digests and engine ID-map policy/ranges; it never owns rootfs storage. |
devices |
Portable requested direct/device/CDI/privileged intent and trusted provenance only; no selected hardware, provider receipt or live handle. |
namespaces |
Requested namespace/donor policy, durable dependency identities, and terminal domain-scoped released-dependency/activation tombstone projections needed to prove finalisation; no live namespace activation, process/sandbox handle, or generic operation/idempotency/retry/outcome/tombstone record. |
logging |
Requested logging policy, portable destination-re-resolvable configuration, protected-option/history disposition, and terminal logging-pipeline/delivery evidence required to prove quiescence; no generic operation/idempotency/retry/outcome/tombstone/finaliser or writer/reader session. |
modelsAndRoutes |
Model settings/content/configuration disposition, portable bindings/environment and protected runtime flags; no runner/transfer/route activation or source provider lease. |
buildsAndCache |
Durable build records/provenance plus content-addressed BuildKit/cache descriptors and verified export/import/rebuild disposition; no active builder/session, secret, credential, mutable helper handle or image record duplicated from imagesAndContent. |
Every other kind owns only the domain named by its enum case. A focused export aggregator must emit its controller records into these canonical parts; it may not create a second “policy/security/rootfs” or “volume/socket/artifact” part. Cross-part references use immutable IDs and are validated after all parts stage, not by duplicating the referenced record.
Generic mutation retry state exists only in identityLifecycleEvents, even when
its operation invokes logging, networking, storage, model, socket, or another
controller. A domain part may carry its requested/effective configuration,
durable leases, provider receipts, and terminal domain evidence needed to prove
quiescence or reconstruct a result, but it references the generic operation ID
and never copies its idempotency key, cached outcome, retry state, tombstone, or
finaliser. Import rejects any generic operation record found in another part.
Image/content import verifies OCI media type, size and digest before promotion;
tag-to-digest collisions require explicit resolution and an unavailable content
blob is retainOffline/re-pull, never a fabricated local image. Build/cache
import accepts only a pinned digest-verifiable BuildKit/OCI cache export
contract. Active builds and cache leases drain before quiescence; unsupported
cache state is explicitly rebuildable/retained offline and cannot block
portable image content. Before commit, either part remains private staged state
tied to the common token and is compensated on abort. The signed commit only
authorises its forward promotion; controller transactions promote it during
reconciling, and it becomes publicly visible only after the signed Complete
outcome establishes destinationActive.
An ordinary move has one source and preserves that authority lineage/key. Wave 8 consolidation may have several independently writable sources (for example, the accepted devcontainer and Container roots). It always stages into a fresh, pristine destination root; an existing prospective destination with data must become another quiesced source instead. The coordinator creates a new resulting lineage and digest key for a multi-source consolidation, preserves every source lineage/key as bounded signed ancestry for unexpired retry records, and requires explicit resolution of every resource/name/idempotency collision. It never silently overwrites destination data or changes an old record's origin scope. Each part lists exactly the source roots whose canonical records it contains.
The identityLifecycleEvents part's canonical bytes normatively include the
operation ledger, every unexpired idempotency success/failure/tombstone with its
lineage/key-version/digest scope, and all pending or completed exit/removal
finalisation records needed for retry/recovery. These records cannot be omitted
merely because no public container is running or an outcome has already been
returned.
Provider change is explicit:
- create one coordinated durable handoff token plus immutable manifest ID,
validate the signed trust-registry revision, prove possession of each selected
destination X25519 key, verify the destination's pristine
nonepair and atomically move it to token-fenceddestinationStagedwith its exact durable abort alternative, move every source todraining, reject new mutations, and advertise draining; - treat every live process generation as non-portable: stop/drain all public
running,paused, andrestartingcontainers plus exec processes, then complete everyProcessExitFinalizationV1; checkpoint/restore is out of scope. Complete or cancel builds, image/model transfers, provider calls, log/attach sessions, readers, and other asynchronous work so only stopped durable records/content can transfer; - reconcile every controller, freeze asynchronous observation/event ingestion,
capture and verify the typed pre-commit header/revision vector for every
sourceQuiescedsource and the data-pristine, token-fenceddestinationStageddestination, stop all source writers from mutating, and advance the exclusive token toquiesced; no snapshot is permitted until every source/controller acknowledges this boundary; - while that token remains held, export one signed/versioned
ProviderHandoffManifestV1whose one immutable payload package per part contains all supported focused records: identity/lifecycle/restart state, the operation ledger, every unexpired idempotency record/tombstone, completed or pending generation-fenced exit and removal recovery marker, protected event-history disposition, images/digests/content, networks/IPAM, volumes/mounts/rootfs/configs/secrets, socket grants, generated-artifact metadata/content, resource/device/security/profile/namespace intent, logging configuration/history disposition, model settings/routes/store references, and build/cache state; - validate every package, capability, signature, envelope, public/source/ aggregate digest, identity, and collision at the destination without public effects, then import all packages through their separate token-scoped mutable staging records as one token-scoped staged import set, or stop for user resolution. This is not one physical cross-controller transaction: each controller owns its private tentative effects/receipt, and only the token's final frozen receipt array plus root prepare/commit protocol coordinates them;
- require every part staging record
imported, freeze the non-cyclicProviderHandoffCommitIntentV1, locally prepare every exact root transition, and execute the signed gateway-decision/root-CAS rule above to select the destination fingerprint/socket discovery and make every source permanently transferred; no individual part can switch ownership or tombstone its source early; - tombstone every source writer/store under that committed token, start only the token-scoped destination reconciliation writer, and reconcile the complete manifest; the ordinary public writer is issued only after the signed Complete transition;
- retain the token, manifest, commit record, and rollback evidence until
destination verification completes. Before the authoritative commit, every
staged destination effect is compensatable/discardable and abort returns the
token to
abortedbefore unlocking all unchanged authoritative sources; after that commit, reversal is an explicit offline reverse handoff and never a dual-writer rollback.
Legacy runtimes that predate v2 generations/finalisers require a durable
LegacyRuntimeQuiescenceInventoryV1, not fabricated v2 acknowledgements. Under
the one migration writer it records each legacy container/VM/process plus
observed mounts, network attachments, device uses, socket relays, log/attach
sessions, and other external handles, with observation revision and
draining|finalising|clear|recoveryRequired. quiesced requires positive
generation-independent proof that every entry is stopped and every listed
effect is closed/released. A crash before that proof leaves the legacy source
authoritative and resumes its drain; an empty v2 ledger is never evidence that
legacy effects are absent.
There is no automatic fallback when the provider is unavailable. Reads may expose a clear unavailable/recovery status, but another provider cannot start writing the same identities.
public enum StateRootHandoffStateV1: String, Codable, Sendable {
case none
case sourceDraining
case sourceQuiesced
case destinationStaged
case sourceTransferred
case destinationReconciling
case destinationActive
}
public struct StateRootHeaderV1: Codable, Sendable, Equatable {
public var schemaVersion: UInt32
public var stateRootUUID: String
public var authorityLineageUUID: String
public var stagedAuthorityLineageUUID: String?
public var currentDataSchemaVersion: UInt32
public var minimumWriterSchemaVersion: UInt32
public var writerEpoch: UInt64
public var selectedProviderFingerprint: String?
public var handoffState: StateRootHandoffStateV1
public var activeHandoffTokenID: String?
public var handoffChainHeadDigest: String?
public var lineageDigestKeyVersion: UInt64
}The state-root UUID never changes. A single-source move preserves its
authority-lineage UUID/protected digest-key version; a multi-source
consolidation creates the manifest's new resulting values and retains the input
lineages only in its signed ancestry index. None is inferred from a provider,
path, socket, name, or matching data. stagedAuthorityLineageUUID is non-nil
only for destinationStaged. The active token is non-nil while a handoff is
in sourceDraining, sourceQuiesced, destinationStaged, or
destinationReconciling, including while an aborting or committed gateway
decision, or a signed terminal outcome, still awaits its physical header
transition. The authoritative Commit
advances the signed handoffChainHeadDigest on every root: sources clear the
active token as they become permanently sourceTransferred, while the
destination retains it through reconciliation and clears it at Complete.
sourceTransferred plus that chain head is the permanent write fence. An
unselected new root may have a nil provider fingerprint; every writable
selected source/destination has exactly the fingerprint named by the gateway
selection record.
Entering sourceDraining or destinationStaged atomically persists the exact
prior header/revision pair in a token-bound root-local handoff journal before the
header transition. That journal is separate lock metadata, survives restart,
cannot be replaced for the token, and is retained through the terminal outcome;
it is the sole restoration source for an abort before pre-commit expectations
freeze.
| Transition | Header compare-and-swap and authority rule |
|---|---|
| New/open | Acquire the inode-safe exclusive lock, validate versions/selection, and increment writerEpoch before issuing a writer-session token. A process without that exact epoch cannot mutate. |
| Drain/quiesce | A selected source in none or destinationActive moves to sourceDraining -> sourceQuiesced with a fresh active token while preserving the prior chain head; provider fingerprint, lineage, digest-key version, and minimum writer floor cannot change. |
| Stage | A pristine destination in none moves to destinationStaged; it contains no prior public resources/idempotency records, and records the manifest's resulting lineage only in stagedAuthorityLineageUUID. An existing data-bearing root must be another source, never an implicit destination merge. No staged record is publicly readable/writable. |
| Abort | Before a valid gateway commit record exists, CAS the token to aborting, discard/compensate every part staging/import record, and release any prepare. After quiescence, compare-and-swap every root from its complete expected pre-commit pair to the signed abort pair; an earlier drain uses the token-bound root-local handoff journal and exact current pair. Both paths restore each source's recorded prior writable state and the pristine destination's prior none state, clear active/staged token fields, retain every prior chain head, and increment each writerEpoch and rootStoreRevision once for the final abort transition. The signed terminal outcome records the full resulting pairs and never claims to reuse the expected pre-handoff values. Every source remains authoritative. |
| Commit | Freeze ProviderHandoffCommitIntentV1 from the exact pre-commit root, manifest, trust/proof, provider-selection and socket-selection expectations. Compute the commit digest from that intent, the new chain head from the commit digest plus ordered prior heads, and only then the post-commit header/revision pairs containing that chain. Each root locally CAS-prepares that exact transition; the signed commit-record decision atomically advances gateway selection/token state and idempotent root CAS applies the prepared pairs. Sources become sourceTransferred; the destination becomes destinationReconciling, adopts the resulting lineage/key/provider and writer floor, and remains token-fenced until Complete. No prepare/signed/post-header value feeds the commit digest. |
| Complete | After full reconciliation and all source tombstoning, destination compare-and-swaps to destinationActive, clears the active token and increments its writer epoch/root-store revision once; every source remains permanently transferred with the same chain head. The signed Complete outcome records all exact terminal pairs. A later forward or reverse offline handoff starts with a new token and chains another commit without erasing the prior fence. |
The gateway commit record is the atomic authority decision even if a crash delays one physical header write; recovery converges headers from that signed record and never infers authority from staged files or running processes. A binary below the minimum writer schema, a non-selected provider, a root claiming another lineage without its verified handoff chain, a stale writer epoch, or a transferred source refuses read-write access. Retained legacy records are rollback evidence exposed only through the migration reader; they are never a second writable view. One selected migration controller may serially operate legacy and v2 runtime mechanisms beneath this lock while draining; an independent legacy daemon/old writer may not.
All family-owned capability and local service identifiers use the owned reverse-DNS root io.github.stephenlclarke.container.*. Existing released Compose capabilities under io.github.stephenlclarke.container.compose.* remain canonical and are not renamed merely for visual consistency. New cross-client contracts use a stable descriptive suffix beneath the same owned root; no design may mint an io.github.container.* identifier.
An identifier's meaning is immutable. A breaking contract receives a new versioned identifier. If a released identifier must be renamed, the provider advertises old and new IDs together only when their semantics are identical, the manifest records the canonical/alias pair and compatibility window, clients prefer the canonical ID, and conformance tests exercise both. The alias is removed only after every supported client/provider bundle has migrated in a declared major compatibility window. Unknown optional capabilities may be ignored; an unknown required capability fails before side effects.
Devcontainer remains usable without container-compose or Stephen's forks:
- its stock provider packages/targets exact upstream Apple Container/Containerization revisions;
- it installs/discovers the neutral
container-enginegateway and registers its stock adapter; - it keeps Dev Container policy above the provider SPI; and
- it can report enhanced-only capabilities unavailable before side effects.
When the enhanced Container-family provider is selected:
- devcontainer discovers the same per-user gateway/socket;
- the gateway calls the matched Container authority through its adapter/XPC contract;
- devcontainer does not start another listener, Apple runtime store, event stream, volume store, or model service; and
- every resource has the same ID/name/state/events across clients.
Non-Apple devcontainer providers may retain their own provider authorities under separate explicit profiles/state roots. They do not claim or merge enhanced Container state.
Socktainer demonstrates a Docker-compatible Unix bridge over Apple Container and transparently relays a requested guest /var/run/docker.sock back to that API. It is an upstream interoperability and conformance input for socket discovery, API behaviour, Podman Desktop integration, and relay security.
It is not embedded as a second daemon and does not own this family's state. Its advertised API target is narrower than this design's Engine API 1.53 target, and its documented limitations do not replace the focused parity contracts. Conformance tests SHOULD run compatible Docker clients and the Podman/Socktainer-shaped discovery/relay cases against container-engine without changing selected authority.
Promote Containerization's experimental multi-container work into one production per-user sandbox. It owns VM boot/capacity, Linux kernel and capability fingerprint, protected host/service namespaces, storage-pool attachment, VM-level network backhaul, guest-agent session, and idle/recovery policy.
All user containers default to private namespaces/cgroups/rootfs/mounts/network endpoints. Only supported PID/IPC/network selections join an immutable donor; supported host selections mean the sandbox's corresponding protected Linux host namespace. Mount namespaces and rootfs always remain per-workload. Privileged exposes the Docker-equivalent Linux host boundary, which is intentionally dangerous to sibling workloads, but the macOS service/VM boundary remains outside.
Feature-specific workload, service, or resource-authority Linux VMs are prohibited. The same sandbox provides:
- dynamic local/remote volume and advanced mount semantics;
- guest per-workload network namespaces and provider endpoints;
- PID/IPC/network donor joins;
- cgroup v2 resources, OOM observations, and live updates;
- device/CDI injection and privileged all-device inventory;
- journald and logging-plugin service workloads; and
- the guest route to host-native Model Runner inference.
The sole VM exception is a non-authoritative Layer2UplinkAppliance when a pinned ipvlan/macvlan success path requires VZBridgedNetworkDeviceAttachment semantics that direct host packet I/O cannot provide. It is one minimal appliance per physical parent, shared by all applicable logical networks, and owns no Engine/provider identity, IPAM/endpoints, workload namespace, durable source of truth, volume, credential, API socket, or user process. The Container network controller remains authoritative, supplies generation-bound effective port state over a private bounded channel, reconciles it after appliance failure, and reports affected networks degraded while unavailable. The appliance is lazy, entitlement-minimal, excluded from bridge, and must pass the same security/fault/performance gates; it is not a second Engine Linux sandbox.
The common create envelope composes, rather than renames, the focused domain DTOs. The following is the cross-design additions and ownership subset of that envelope, not a complete replacement for the existing create request and not a second compilable Swift wire declaration. Existing canonical image reference, platform, process/command, environment, user/group, working-directory, port, hostname, and other already-supported create fields remain in the full wire request unchanged and are consumed by the same transaction:
ContainerCreateRequestV2
schemaVersion -> explicit root envelope version
idempotencyKey -> native/client key or gateway-minted internal operation identity
identity -> WorkloadIdentityRequest
lifecycle -> ContainerLifecycleIntentV2
linuxPolicy -> ContainerLinuxWorkloadPolicyRequestV1
namespaces -> LinuxNamespacePolicy
privilege -> ContainerPrivilegeIntentV2
devices -> ContainerDeviceRequestSetV1
(Docker requests plus optional trusted diagnostic provenance)
resources -> ContainerResourceIntentV2
(mounts, configs, and InboundUnixSocketIntentV1)
protectedPayloads -> authenticated non-Codable artifact sidecar keyed by artifactID
networkMode -> RuntimeNetworkModeIntentV2
networks -> [RuntimeNetworkAttachmentRequest]
logging -> ContainerLogRequest
modelRoute -> ModelRouteIntent?
The root request carries an explicit schemaVersion; HTTP route/API-version
negotiation and capability IDs do not substitute for it. The lifecycle-owned
neutral DTOs are:
public struct WorkloadIdentityRequest: Codable, Sendable, Equatable {
public var schemaVersion: UInt32
public var requestedName: String?
public var labels: [String: String]
}
public enum ContainerRestartPolicyNameV2: String, Codable, Sendable {
case engineDefault = ""
case no = "no"
case onFailure = "on-failure"
case always = "always"
case unlessStopped = "unless-stopped"
}
public struct ContainerRestartPolicyV2: Codable, Sendable, Equatable {
public var fieldWasPresent: Bool
public var name: ContainerRestartPolicyNameV2
public var maximumRetryCount: Int64?
}
public enum ContainerHealthcheckTestV2: Codable, Sendable, Equatable {
case disabled
case exec(arguments: [String])
case shell(command: String)
}
public struct ContainerHealthcheckIntentV2: Codable, Sendable, Equatable {
public var test: ContainerHealthcheckTestV2
public var intervalNanoseconds: Int64?
public var timeoutNanoseconds: Int64?
public var startPeriodNanoseconds: Int64?
public var startIntervalNanoseconds: Int64?
public var retries: Int64?
}
public struct ContainerLifecycleIntentV2: Codable, Sendable, Equatable {
public var schemaVersion: UInt32
public var restartPolicy: ContainerRestartPolicyV2
public var autoRemove: Bool
public var stopSignal: String?
public var stopTimeoutNanoseconds: Int64?
public var healthcheck: ContainerHealthcheckIntentV2?
}
public struct SensitiveArtifactPayloadV1: Sendable {
public var artifactID: String
public init(artifactID: String, boundedBytes: Data) throws
public func withUnsafeBytes<Result>(
_ body: (UnsafeRawBufferPointer) throws -> Result
) rethrows -> Result
}
public struct SensitiveArtifactPayloadSidecarV1: Sendable {
public var schemaVersion: UInt32
public var payloads: [SensitiveArtifactPayloadV1]
}
Optional retry, duration, signal, and healthcheck fields preserve omission
separately from explicit zero/empty values; validation and default resolution
occur at the pinned Engine phase. The signed retry count deliberately retains a
negative raw Engine value until phase-correct validation. fieldWasPresent plus
.engineDefault preserves an omitted policy, an explicitly empty name, and
explicit no separately until the oracle proves any collapse. A nil healthcheck
means inherit/no explicit container override, while .disabled is an explicit
disable. These definitions live in container-engine-api; Compose,
devcontainer, and Apple adapters do not declare lookalike forms.
The protected payload sidecar is a separate authenticated in-memory/streaming channel, not a field admitted to the general JSON/XPC persistence encoder. Every payload ID must be unique, bounded, and referenced by exactly one config/artifact intent that declares new inline sensitive content; missing, duplicate, unexpected, or mismatched IDs fail before public identity/resource effects. Before create commit, Container seals each payload into its protected artifact store, verifies content digest and inode identity, and records only artifact ID, protected HMAC/content digest, metadata, and content revision. The public create record never contains bytes. Pre-commit failure compensates only artifacts created by that attempt; a Docker-oracled post-create projection failure retains the stopped container and its sealed owned artifact. Logging, diagnostics, events, dry-run, and provider calls never receive the bytes.
WorkloadIdentityRequest and ContainerLifecycleIntentV2 are owned by the
lifecycle contract. Their shared definitions are:
| DTO | Field | Exact requested meaning |
|---|---|---|
WorkloadIdentityRequest |
schemaVersion |
Required identity-contract version; the root create envelope has its own version and either unsupported value fails before identity allocation. |
WorkloadIdentityRequest |
requestedName |
Optional client name, canonicalised and collision-checked when present; it is never an ID or storage path. When absent, the authority generates/reserves one canonical Docker-compatible name inside the create transaction and persists it in the attempt record before any retryable effect. Compose always supplies its deterministic name. |
WorkloadIdentityRequest |
labels |
Exact key/value content after protocol decoding, including Compose project/service/replica provenance where applicable; canonical sorting is used only for hashing/encoding. |
ContainerLifecycleIntentV2 |
schemaVersion |
Required lifecycle contract version. |
ContainerLifecycleIntentV2 |
restartPolicy |
Presence-aware typed Engine name (empty/default, no, on-failure, always, or unless-stopped) plus optional signed raw maximum retry count, retaining omission/empty/zero/negative through pinned validation. No internal .none spelling crosses the wire. |
ContainerLifecycleIntentV2 |
autoRemove |
Exact Docker create intent; removal still uses the canonical removing/dead transaction. |
ContainerLifecycleIntentV2 |
stopSignal |
Optional requested signal, retaining omission for Engine default resolution. |
ContainerLifecycleIntentV2 |
stopTimeoutNanoseconds |
Optional non-negative timeout with omission distinct from zero. |
ContainerLifecycleIntentV2 |
healthcheck |
Optional typed test/disabled disposition, interval, timeout, start period, start interval, and retries, with presence retained for Docker projection. |
Every other type in the map is the canonical requested DTO in its linked
focused design. All focused designs import these exact definitions from the
shared Engine API module rather than defining lookalikes. Container-internal
effective plans use different names and may contain stable provider handles;
clients never author or send those handles. The sole echo exception is an
authority-issued, command-scoped authenticated ModelRouteIntent: it contains
only binding/command identity, expiry, digest and MAC, exposes no provider
fingerprint/revision/contract, and is accepted only by the issuing authority to
look up its private resolution before create effects. It is not a reusable
provider handle or a client semantic choice. The authority adds the allocated
containerID, selected provider fingerprint, immutable bundle key, initial
transitionRevision, and operationGeneration; none is client-supplied.
An idempotency identity is scoped by the stable Engine-authority lineage UUID,
authenticated caller namespace, operation kind, and idempotencyKey. It is not
scoped by the replaceable provider fingerprint or physical state-root UUID. The
authority persists a canonical semantic request digest
before effects. Sensitive payloads contribute a stable protected HMAC/content
digest rather than plaintext, and diagnostic-only provenance does not alter the
semantic digest. A retry with the same scoped key and digest joins the in-flight
operation or returns its cached success/failure and exact resource identity. Reuse
with a different digest is a conflict and can never alias or mutate the earlier
operation.
Native and Compose callers may provide a stable idempotency key. Docker Engine API 1.53 has no such client field: the HTTP gateway mints one opaque key per accepted request and reuses it only for that request's internal dispatch and crash recovery. A client resubmission is a new Docker operation and therefore gets ordinary name-conflict behaviour or, when no name was supplied, a second generated container. No private header or stronger replay guarantee is added to the Docker wire. For a native same-key retry with an omitted name, the attempt record returns the originally generated name/container identity; name collision generation is never rerun for that operation.
The full idempotency record lives for the resource lifetime. After terminal removal it becomes a compact outcome tombstone retained for the advertised retry window; expiry and garbage collection are atomic and observable only in protected diagnostics. Reuse after tombstone expiry is a new operation, so clients requiring longer replay safety must supply a fresh unique key. Handoff migrates unexpired records/tombstones with their unchanged origin-lineage scope, key version, and digest. A single-source destination continues that lineage. After multi-source consolidation, new operations use the resulting lineage; old retries consult only a bounded signed ancestry index for the same authenticated caller, operation kind, and key, then recompute with that record's original protected key/version. No unrelated state root is searched.
If more than one source contains the same caller/operation/key scope, consolidation must stop until one record expires or a source-qualified client mapping is explicitly installed. It may deduplicate only records whose semantic digest, exact outcome, and resource identity are all equal and whose signed sources approve that alias. It never silently chooses one client's record, creates a broad old-to-new alias, or re-scopes an old record. The ancestry index and its key versions are garbage-collected only after their last unexpired record/tombstone.
Sensitive semantic digests use a versioned authority-lineage HMAC key held only in the protected authority store. It is not state-root-local. The signed manifest carries a destination-sealed envelope for every source key version needed by an unexpired record/tombstone and for the resulting lineage's key; staging rewraps them into the destination protected store. A single-source commit activates the transferred key; a consolidation activates the new key for new operations while ancestry lookup retains old keys only for old retries. Raw key bytes never enter a provider, normal manifest inspection, log, or diagnostic. Rotation retains old key versions only for citing records, with an explicit version alias window. This preserves privacy and post-cutover digest equality without allowing unrelated roots to compare payloads.
The resolver turns this requested policy into an effective plan containing only stable IDs, provider handles, profile digests, namespace handles, cgroup values, OCI edits, mounts, endpoints, model-route handles, and controller-scoped protected-effect references. A reference contains a stable effect ID, owning controller/provider generation, opaque protected-store object ID and integrity digest; it is not the raw bearer token, credential, secret, private host path, or provider request body. Only the owning controller resolves the reference inside its protected process at the exact provider call, bounds its lifetime to that call/session, and never serialises the raw value into Compose, the shared plan, the operation ledger, logs, events, diagnostics, or handoff content.
The operation ledger records phase, controller, owner ID, generation/revision, idempotency key, protected-effect reference/digest, commit, and compensation. The owning controller persists any required raw effect material only in its protected store, rotates or revokes it independently, and resolves it just in time for the controller/provider call. Controllers retain their own domain stores and APIs; the ledger coordinates their policies and leases without collapsing them into one class.
| Term | Owner and change rule | Contract use |
|---|---|---|
Container incarnation / containerID |
Authority-allocated immutable 64-hex ID for one create-to-remove record; rename never changes it and recreation creates another ID. | Canonical owner for lifecycle, events, intents, leases, logs, and lookup. |
transitionRevision |
Monotonic per-container state/event commit revision. | Compare-and-swap and ordered inspect/event projection; never a process or provider generation. |
processGeneration |
Candidate reserved by the lifecycle controller for each start/restart attempt and committed only when that workload process starts successfully. | Exit/wait/restart suppression and every live process/session/activation handle. A failed candidate is compensated and retained only in the operation record. |
operationGeneration |
Monotonic authority transaction attempt paired with the client idempotency key. | Retry, recovery, compensation, and event causality; never identifies a running process. |
restartPolicyRevision |
Monotonic lifecycle-controller revision that changes whenever the requested/effective automatic-restart policy changes. | Invalidate pending restart timers without reusing the resource/security policyRevision; a pending timer is also bound to its ledger operationGeneration. |
policyRevision |
Monotonic per-container requested/effective resource and security policy revision owned by the policy controllers. | Inspect/update/rollback truth; live cgroup/profile handles additionally bind to process and sandbox generations. |
idMapRevision |
Engine ID-map controller; changes when the configured UID/GID mapping policy changes. | Pin the mapping used to resolve workload, mount, socket, config, and secret ownership; it is an engine configuration revision, not a provider/inventory or lease generation. |
sandboxGeneration |
EngineLinuxSandbox manager; changes when the sandbox/guest-agent runtime is replaced or boots into a new incarnation. |
Reject/reconcile stale guest handles while preserving authority-owned workload intent. |
providerGeneration |
Common provider registry; changes when the selected implementation/contract incarnation is replaced. | Route every opaque provider ID/protected-effect reference and fence capability/contract decisions; another generation never interprets or resolves it. |
inventoryGeneration |
One exact providerID plus providerGeneration; changes when that provider's observed inventory snapshot changes without requiring an implementation change. |
Fence selection/availability decisions. It is meaningful only with its provider identity/generation and never substitutes for providerGeneration. |
Domain leaseGeneration |
Specialised controller; changes when that durable lease record is replaced or materially revised. Every durable child record uses this exact field name; provider, inventory, contract, session, and content generations are separate clocks rather than aliases. | Stable ownership/recovery of network, volume/mount, rootfs-storage, device, namespace, profile, logging, socket, or model-route intent. |
Durable container intent survives an ordinary stop when Docker retains it:
endpoint/address reservations become inactive; volume, rootfs-storage, and
namespace leases remain owned; requested/effective resource policy and its
policyRevision remain while the live cgroup is removed; the requested
DeviceRequest remains with the stopped container, but its active
DeviceRequestResolutionV1 and all member leases/handles are released. During
automatic-restart backoff only, a provider MAY retain the focused device
design's explicitly deactivated request-level reservation when that is safe and
required to prevent reallocation; it binds again only after a new resolution
against current inventory and the verified new processGeneration. Logging
configuration/history,
socket-grant intent, model-route intent, security/profile snapshots, and
generated artefacts also remain. Live processes, attach/exec/log-writer
sessions, guest mount/device handles, relays, and route activations are keyed by
containerID plus processGeneration and relevant sandbox/provider generation.
The lifecycle-owned ProcessExitFinalizationV1 deactivates them after every
natural exit, stop, kill, and automatic/explicit restart, not only an explicit
stop; it persists controller acknowledgements before a new generation may
activate. They are recreated only for a verified new running process
generation. Remove releases durable intents in reverse dependency
order. No document may use an unqualified containerGeneration or
workloadGeneration as a substitute for these clocks.
The lifecycle design is a foundation, not a later presentation feature. Immutable ID and mutable name must exist before namespace donors, volume/device leases, event actor IDs, socket grants, or migration can be correct.
State mutation and its canonical Docker event commit in one ordered authority transaction. Resource controllers return acknowledged outcomes; they do not publish competing Docker actions. Internal provider/model/logging health uses namespaced protected diagnostics, not fabricated container events.
Container owns requested/effective network resources, IPAM leases, endpoints, DNS, port forwards, and provider lifecycle. VM backhaul is stable; guest per-workload endpoints are dynamic. Network joiners delegate all endpoint identity/ports/DNS to donor ID. No VZ NIC is treated as a container namespace.
The shared Linux-native storage pool is attached once. Volumes, NFS/CIFS, brokered block/NBD, bind grants, rootfs uppers, mount propagation, and /dev/shm are dynamically resolved into workload mount namespaces. An oracle-qualified host managed-directory path may optimise eligible semantics but remains under the same lease/authority model.
Sandbox capacity is admission policy; per-container CPU/memory/PIDs/block I/O are cgroup settings. Unsupported cgroup-v1 controls follow Docker's cgroup-v2 warnings/discard behaviour and remain requested versus effective.
The workload resource/cgroup controller owns per-container resource policy, cgroup hierarchy/settings, OOM counters, update/recovery, and writable-cgroup mount disposition, consuming privilege and namespace policy where required. The ID-map/profile controller owns engine user-remap policy, inline immutable seccomp content, AppArmor/SELinux capability behaviour, mappings, and profile digests. Containerization performs the generic cgroup mount/materialisation from their combined effective plan. DeviceBroker owns direct devices, GPU subset, CDI, provider-specific options, all-device privileged expansion, health, and conflicts with rootfs/volume block leases.
Valid Local Deploy scheduler-only fields are preserved in config and ignored locally; schema-invalid resource locations retain compose-go errors. Reservation DeviceRequests use the same DeviceBroker as service devices, gpus, CDI, and privileged inventory.
The logging controller owns one driver registry and per-container/per-generation pipeline. Built-in none, json-file, and local remain distinct. Remote/native drivers run through bounded providers; Linux journald and plugin workers are protected service workloads in the common sandbox. Privileged workloads are not isolated from the Linux host/service plane by default, so the hard outer boundary remains macOS/VM.
The pinned Docker Model Runner is host-native for Metal and global OCI model state. In enhanced mode, the selected Container authority alone supervises it and owns its store/credentials beneath that provider's state root. container-engine only dispatches the neutral model SPI to the selected provider; a stock adapter reports the capability unavailable unless it supplies its own isolated full implementation and never accesses enhanced state.
Every endpoint claim is durably scoped to the exact command ID, command create
window, authenticated caller namespace and caller idempotency key, and conflicts
unless the complete caller-owned endpoint-request semantic digest also matches.
The authority-issued ModelRouteIntent echoes that one claim only inside its
still-open create window; it cannot be replayed by another command, window,
caller, request digest, lineage or provider, and a lost response reconciles the
original claim instead of minting another binding.
Model configuration keeps caller intent separate from authority effects. The caller owns and can replay the complete non-secret command request and semantic intent digest; the selected authority alone assigns the internal effect ID, protected-effect reference, predecessor and monotonically ordered per-key configuration sequence, and durably maps them to that request before the runner call. Callers never author or need those effect/sequence fields. Same-request recovery rejoins that mapping and exact outcome, while a later command receives its own ordered effect even when desired configuration is identical. Raw protected values are resolved only inside the model controller/runner adapter at dispatch, consistent with the common ledger contract above.
For a bound workload, Compose passes the derived environment overlay plus a typed
ModelRouteIntent. The Container model-route controller resolves it against the
effective network namespace and creates/returns a ModelRouteLease owned by the
container ID and domain leaseGeneration through the common ledger; its live
activation is keyed to the verified processGeneration. The proxy may be shared,
but start activates it, ProcessExitFinalizationV1 deactivates it after every
process exit, and remove releases it. A namespace joiner persists its delegated
donor disposition and creates no duplicate endpoint. network_mode: none
persists the inspectable durable .intentionallyUnreachable no-route disposition
and Docker-visible environment, but creates no preparation, activation, provider
call, route effect, or handoff effect. Route cleanup never unloads global model
content. Pull/configure/credentials remain host-only. A model binding is not an
ACL, and service Deploy GPU reservations do not schedule host Model Runner
compute.
The gateway socket is current-user private and broad authority. use_api_socket creates a typed container grant and credential-config projection; guest relay targets the same listener/selected provider. Privileged, model binding, namespace host mode, or a mounted volume never implies this grant.
| Boundary | Trust and protection |
|---|---|
| macOS user to another user/system | Per-user launch/service identity, private directories/socket permissions, peer credentials, Keychain ACL, no cross-user discovery. |
| Engine gateway to provider | Versioned authenticated local transport, immutable fingerprint, capability negotiation, request/idempotency identity, no automatic provider switch. |
| macOS authority to guest | Host-authoritative guest-agent protocol, generation-bound opaque leases, no broad credentials in guest, bounded messages and replay protection. |
| Baseline workload to sibling | Private namespaces/cgroup/mount/rootfs/network, explicit devices/mounts/routes/capabilities, no socket by default. |
| Privileged workload to Linux host/sibling | Deliberately weak/Docker-equivalent; document host-dangerous authority. Hypervisor/macOS service remains the boundary. |
| Workload to Engine API | No access unless an explicit use_api_socket/bind grant exists; grant is full authority and audited. |
| Workload to Model Runner | Inference-only controlled route; management/store/credentials remain host-only. Docker-compatible shared inference is not misrepresented as per-binding authorisation. |
| External providers/plugins | Signed/authorised manifests, narrow capabilities/export roots, schema/bounds, secret handles, timeouts, process isolation, health and revocation. |
Every destructive cleanup uses stable owned IDs/leases, never mutable name, broad glob, unresolved environment variable, or unverified provider path. A partial cleanup remains visible through the lifecycle dead state and resumable ledger.
The graph defines architectural dependency order. The development cycle defines how each vertical slice is implemented, reviewed until clean, validated, landed, handed off, and closed without dangling GitHub state.
flowchart LR
O["0. pinned cross-client oracles"] --> E["1. neutral Engine API and exclusive provider SPI"]
E --> I["2. immutable identity, lifecycle journal, workload ledger"]
I --> S["3. production EngineLinuxSandbox"]
S --> N["4a. guest network and IPAM endpoints"]
S --> V["4b. storage, mount and rootfs plane"]
S --> R["4c. per-workload cgroups and profile/ID-map plane"]
N --> D["5. DeviceBroker, namespace donors and privilege"]
V --> D
R --> D
I --> LC["logging core and built-in file drivers"]
LC --> L["6a. complete logging pipelines"]
D --> L
N --> M["6b. Model Runner guest route"]
E --> M
D --> M
D --> C["7. Compose typed projection and gap removal"]
L --> C
M --> C
C --> X["8. devcontainer migration and cross-client cutover"]
X --> A["8b. singular-authority use_api_socket enablement"]
A --> G["9. integrated fault, security, migration and performance gates"]
Stable ID: COHERENT-WAVE-00.
- Pin Docker Compose 5.3.1, its embedded compose-go 2.13.0, Engine 29.2.1 API 1.53, Docker Model Runner, the repository's compose-go 2.14.0 candidate, Container/Containerization, guest image, and accepted devcontainer extraction revision.
- Treat the compose-go 2.13.0/2.14.0 skew as an explicit compatibility input. Either align the normaliser or run black-box differential coverage for schema, interpolation, merge, include, extends, project discovery/loading, paths, profiles, and hashes; record every intentional extension. Byte-identical Deploy device/schema files support only that focused conclusion and cannot prove whole-project parser parity.
- Build reusable oracles for Engine routes, state/events, namespaces, cgroups/security, devices, networks, volumes/mounts, logging, models, socket relay, failure phases, and performance.
- Record host/kernel/controller capability fingerprints and distinguish environmental unsupported behaviour from design gaps.
Stable ID: COHERENT-WAVE-01.
- Extract the neutral Engine wire/router/server/test package from a clean devcontainer head.
- Generate/maintain the API 1.53 route/field/capability ledger and close known bounded-router gaps.
- Implement one
container-engineservice, stock/enhanced adapters, immutable fingerprint, exclusive selection, discovery, and drain/handoff protocol. - Keep existing products on adapters until cross-client wire/performance evidence passes.
Stable ID: COHERENT-WAVE-02.
- Split immutable ID/mutable name/bundle/provider identity and migrate indexes.
- Implement canonical lifecycle snapshot, state transition controller, generation-aware operations, event journal, and removing/dead recovery.
- Add the workload transaction ledger and typed controller lease contracts.
No namespace, API-socket, devcontainer resource import, or complete event claim proceeds before this wave.
Stable ID: COHERENT-WAVE-03.
- Refactor
LinuxPod/LinuxContainerintoEngineLinuxSandboxplus complete per-workload configuration. - Add protected host/service namespaces, dynamic workload namespace/cgroup/rootfs/process lifecycle, guest-agent recovery, storage pool, and VM backhaul.
- Retain compatibility wrappers only for migration; do not build new parity features into the old per-container topology.
Stable ID: COHERENT-WAVE-04.
- Implement dynamic guest networking/IPAM/DNS/ports, custom-name existing-network mode resolution, and provider contracts.
- Implement shared storage, volume providers, mount propagation/recursive modes, rootfs storage options, bind/block brokers, and leases.
- Split workload resources from VM capacity; implement cgroup capabilities, user-remap, profile snapshots, security options, OOM observations, and requested/effective inspection.
These may proceed in parallel after the sandbox/ledger contracts freeze, but integrated create/remove failure tests gate the next wave.
Stable ID: COHERENT-WAVE-05.
- Implement DeviceBroker providers/CDI/inventory/leases and block conflicts.
- Implement arbitrary namespace donor graphs/dependency reconciliation.
- Complete privileged create/exec policy and outer-boundary tests.
- Correct Local Deploy preservation/ignore mapping and route all reservation devices through DeviceBroker.
Stable ID: COHERENT-WAVE-06.
- Add distinct built-in logging formats/rotation/read contracts and provider registry/delivery/buffering; place Linux providers in sandbox service namespaces.
- Pin/supervise host-native Model Runner, global store/credentials, inference-only gateway route, and exact Compose ensure/injection phases.
These build on identity/network/security foundations. The host-native Model
Runner and protected logging/provider service workloads retain their own
service lifecycle and never appear as user containers or fabricated Docker
states. Per-container logging pipelines and model-route leases are nevertheless
workload-ledger children: create persists their intent, start activates them,
ProcessExitFinalizationV1 deactivates them after natural exit, stop, kill, or
restart, and remove releases them in the canonical dependency order.
Stable ID: COHERENT-WAVE-07.
- Replace Compose CLI-shaped runtime hand-off with typed v2 policies/capability preflight.
- Exercise each completed authority/controller contract through Compose reconciliation without manufacturing state.
Stable ID: COHERENT-WAVE-08.
- Execute one quiesced, exclusive, collision-aware provider handoff using the common manifest, token, staged import, and single commit defined above. It places every focused devcontainer record in exactly one canonical part package; lifecycle, resource, device, event, model, image, network, volume, socket, log, and build/cache migrations are not independent cutovers.
- Change enhanced devcontainer to socket discovery only and retain a tested stock standalone provider.
- Drain legacy per-container VMs and rematerialise their imported stopped records in the shared sandbox under the same v2 container IDs; never fake live namespace import. Only ordinary Docker/Compose replacement semantics create a new ID.
Stable ID: COHERENT-WAVE-08B.
- Enable the already typed/gated Compose
use_api_socketprojection only after the devcontainer cutover proves one exclusive gateway, authority, identity store, event source, and resource state root. - Prove full-authority root/non-root socket use, credentials, revocation, restart/recovery, recursive client-created resources, and zero-artifact opt-out through the singular authority before integrated release gates.
Stable ID: COHERENT-WAVE-09.
- Implement and run the remaining macOS parity closure: reconcile every STATUS requirement, classify provider feasibility, and execute the complete cross-client fault, security, migration, rollback, compatibility, and release-build performance matrices against immutable candidates.
- Move all stack pins/manifests/docs together, with an explicit rollback stack.
- Publish only after exact-head GitHub-recorded authorities pass and the matched stack satisfies the integrated definition of done.
- A command establishes the base gateway protocol, selected-provider fingerprint, state-root identity, and, only when the operation needs it, current sandbox reconciliation token before authority-owned unsafe effects. Feature capability checks occur at the focused design's pinned Docker command phase and preserve any earlier reference-permitted effects; logging catalogue discovery and model capability checks cannot be promoted into a generic zero-residue preflight.
- Every mutation has a mandatory internal ledger idempotency key, immutable resource ID, state revision, and operation generation; native/Compose clients may supply the key, while the Docker gateway mints it per accepted request.
- State/event commit is atomic; provider side effects are staged/reconciled through ledger claims and protected-effect references/digests, with raw material resolved only by the owning controller at dispatch.
- Independent reservations may run concurrently, but final materialisation uses the complete effective plan.
- Failure compensates completed leases in reverse dependency order and retains actionable dead/recovery state when compensation cannot finish.
- Gateway disconnect/cancellation does not undo a committed operation or cancel shared work required by another client.
- Provider/authority/guest restart first reconciles actual resources, leases, and unpublished events; it does not assume process death cleaned anything.
- A mismatched provider fingerprint cannot inspect/mutate another provider's state as if compatible.
- Migration collision or unverifiable ownership stops for explicit resolution. Automatic name/ID/data merge is prohibited.
Run the same scenario through native Container, Container Compose, Docker CLI/API, and devcontainer, then compare stable ID/name, inspect, events, files/network/process behaviour, errors/warnings, and cleanup:
- create/start/attach/exec/update/restart/wait/stop/remove and all public lifecycle states/events;
- private/host/donor namespace graphs, donor recreate, and privileged/security/device combinations;
- custom IPAM/providers, IPv4/IPv6-only, network joiners, ports/DNS;
- local/non-local volumes, recursive/propagation binds, copy-up, storage options, multi-container sharing;
- every logging driver mode/read/rotation/buffering/failure/restart path;
- model selection/pull/configure/injection/global persistence/network endpoint;
- Local Deploy mapped and preserve-but-ignore fields;
- explicit API-socket mounts, credential transformation, relay, and full-authority actions; and
- stock/enhanced provider selection and no-fallback failure.
Inject failure before/after every ledger phase and restart gateway, authority, guest agent, sandbox VM, provider/plugin, logger, Model Runner, and client. Prove no duplicate identity/event, leaked IP/mount/device/socket/profile/namespace/cgroup lease, corrupt data/model, false success, or stale restart resurrection.
Exercise malicious HTTP framing/streaming/archive requests, Unix socket replacement/symlinks/peer users, forged provider/lease/generation IDs, provider path escape, malicious guest requests, credential/log redaction, profile TOCTOU, device/CDI injection, privileged macOS escape attempts, and Engine-socket negative cases.
Common-ledger tests substitute, remove, replay across provider generations and digest-mismatch every protected-effect reference. Generic plan/ledger/handoff/ inspect/log/event encoders reject raw bearer material, while the owning controller proves just-in-time resolution only for the exact provider call. Model tests additionally replay endpoint claims across command IDs, create windows, callers and semantic request digests, and prove that callers cannot author authority effect IDs, predecessors or configuration sequences.
Handoff has maintained golden and mutation suites:
- Deterministic-CBOR fixtures cover NFC, key ordering, integer/length boundaries, duplicate/indefinite/tag/float rejection, and byte-string rather than hex digest projection. Known-answer vectors independently recompute revision-vector, state-header, manifest, signature-projection, commit-intent, chain-head, post-header/vector, root-prepare, terminal-outcome, public-content, source-HMAC, aggregate, AEAD-AD, HKDF salt/key, and transport digests.
- Commit fault injection changes each controller/root-store revision, checkpoint, pre-header, prior chain, manifest/proof/trust binding, provider selection, and socket discovery value. It crashes before/after every root prepare, prepare acknowledgement, coordinator decision and physical post-pair write. A prepare with a changed pre/post header/vector digest is rejected; no-decision recovery releases all prepares before abort or resumes the same intent, while valid-decision recovery only rolls forward. A changed post-header/vector, prepare record or signature cannot change the intent digest/chain head, and any non-derived value is rejected rather than creating a cycle or second authority.
- Trust/crypto tests reject wrong algorithm, purpose, role, provider/root, provenance/enrollment proof, registry revision, validity window, rotation predecessor, revocation, key/signature length, non-canonical/low-order X25519 key, all-zero shared secret, failed destination possession proof, reused ephemeral/nonce, and swapped payload/envelope keys. Rotation/revocation tests retain each bound destination private key through terminal wipe and permit only the declared abort/roll-forward recovery path. RFC-compatible X25519 plus independent HKDF-SHA-256 and XChaCha20-Poly1305 known-answer implementations must agree.
- AEAD mutation changes every associated-data field and verifies the fixed
objectLocalIDmapping and every required/nil applicability cell. Substituting the laterbundleObjectID, ciphertext, transport digest, manifest digest, commit value, or post-header into AD/KDF is rejected by schema/golden tests. Truncation, tag substitution, wrong destination, and same-token/different-manifest replay fail before plaintext is admitted to staging. - Content tests cover public packages, one-source protected packages, two or more source partitions, multiple secrets/artifacts inside one payload object, source/key-version rotation, wrong/missing signed key envelopes, reordered or duplicate entry IDs, unassigned/cross-source entries, per-source HMAC mismatch, and aggregate mismatch. No protected plaintext SHA is accepted as a substitute.
- Staging tests lose/replay/reorder received ranges and responses, restart at
every state, substitute a descriptor/import receipt, and attempt to mutate
signed manifest bytes or add a per-entry object ID. Only the one descriptor
object can reach
imported; abort compensation is durable and resumable. - Ownership fixtures place a generic operation/idempotency/retry/outcome/ tombstone/finaliser in each non-identity part and require import rejection. Logging accepts only its domain configuration/history and terminal pipeline or delivery evidence needed for quiescence. Namespaces accepts its terminal domain-scoped released-dependency/activation tombstone projections, rejects live activations/handles, and rejects a generic tombstone disguised as domain finalisation evidence.
Every negative case fails before authority commit and retains recoverable source ownership. Post-decision injected failures retain the signed decision, keep old writers fenced, and converge forward without recomputing an epoch or digest.
- Legacy Container stopped/running resources and volumes.
- Original and accepted-head devcontainer databases, containers, networks, managed volumes, image records/content, build/cache state, names/IDs/digests, and conflicts. Import verifies immutable IDs and content digests; unexportable state receives an explicit retain/re-pull/rebuild disposition; same-tag with different-digest never silently merges; the old writer and every migrated store are tombstoned after cutover.
- Identical, same-name/different-ID, same-ID/different-provider/state, and interrupted handoff.
- The closed part inventory has exactly one immutable payload object per kind; that object may contain an ordered inline package of multiple records, including multiple protected entries, but no entry has its own object ID. No duplicate rootfs, artifact, socket-grant, generic operation or policy record is permitted; cross-part references resolve after staging. Empty/unsupported/unresolved dispositions still have evidence entries, while missing/oversize objects and media/schema mismatch fail deterministically.
- Single- and multi-source migrations independently verify every signed lineage key envelope, source partition HMAC and ordered aggregate after decryption; wrong source/key-version binding cannot be resolved by rehashing plaintext.
- Resumable mutable staging records survive restart without changing the signed manifest. Crash matrices before the decision permit abort; matrices after the signed decision repair the exact derived provider/socket selection and prepared post-commit header/revision pairs, never restore a transferred source.
- Pre- and post-cutover retries of every unexpired idempotency record return the same outcome and resource identity under the transferred authority lineage; another state root with the same caller/key cannot discover or reuse it.
- Stock to enhanced, enhanced to stock where semantically exportable, failed import, rollback, old-binary rejection, and schema downgrade.
- Dirty checkout is never used as migration evidence.
Use matched non-debug bundles and retain raw monotonic evidence, environment, exact heads, workload, repetitions, median/P95, and noise method. Cover:
- Engine HTTP ping/version/list/inspect/stream/archive/attach;
- cold/warm sandbox and 1/10/50 workload create/up/down;
- network/volume/device/profile/namespace allocation;
- logs first frame/tail/follow and remote delivery;
- model cold/warm/concurrent preparation and endpoint recovery;
- cross-client operations and provider reconnect; and
- migration/recovery where relevant.
Performance does not compensate for behavioural divergence, and parity does not imply acceptable performance.
- Follow the Container-family parity development cycle for slice scope, proportionate local testing, full review convergence, self-hosted MBP execution, main checkpoints, quality scanning, and upstream handoffs.
- Add
container-engine-apiand accepted devcontainer revision to the common stack manifest/fingerprint and compatibility documentation. - Pin Container, Containerization, guest image, builder components, Container Compose, Engine gateway/API package, devcontainer provider, logging/model dependencies, and API schemas as one tested release set.
- Use semantic immutable releases for the neutral package and one mutable
currentonly where the existing family release contract permits it. - Generate compatibility matrices from provider capability manifests and oracle results; do not hand-maintain unsupported claims separately in each project.
- Changes to a shared DTO/route/lease/event require provider and cross-client contract tests before merge.
- Status rows close only after the exact final published heads and GitHub-recorded gates/authorities pass; local design or unit evidence alone is insufficient. Those jobs may execute on the trusted self-hosted MBP lane.
| Area | Required proof |
|---|---|
| Shared gateway | One hardened API 1.53 listener/router package is used by devcontainer and the Container family with a complete route/field ledger. |
| Provider authority | Exactly one fingerprint/state writer exists; stock and enhanced lanes drain/switch explicitly with no fallback or federation. |
| Devcontainer | Remains stock-Apple standalone while enhanced mode discovers one shared socket/authority and owns no duplicate resources/events. |
| Sandbox | One production Linux kernel supplies isolated per-workload namespaces/cgroups/network/mounts and arbitrary donor graphs. |
| Identity/lifecycle | Immutable ID, mutable name, complete Docker states/actions, event journal, waits, OOM, and dead recovery are canonical across clients. |
| Transactions | Typed controller leases, common-ledger protected-effect references/digests with controller-only raw resolution, and durable reverse compensation prevent partial cross-resource creation/removal. |
| Network/storage | Advanced provider/IPAM and volume/mount/rootfs semantics use the sandbox topology without per-workload VZ hardware or unsafe multi-attach. |
| Resource/security/device | Workload cgroups, requested/effective capability behaviour, ID maps/profiles, DeviceBroker/CDI, Local Deploy subset, and privilege pass. |
| Logging | Distinct built-ins and provider delivery/read/buffering semantics use one registry and common sandbox service plane. |
| Models | Pinned host-native runner, global store, command/create-window/caller/request-digest-bound endpoint claims, caller-intent versus authority-effect/sequence separation, secure inference route, exact ensure/injection, and shared authority pass. |
| Socket security | use_api_socket/bind relay targets the same gateway, is explicitly granted/full authority, and is never implied by privilege. |
| Socktainer interoperability | Compatible discovery/client/relay scenarios pass without introducing a second daemon or narrowing the API target. |
| Handoff commit | Golden implementations reproduce the typed revision-vector/header, commit-intent, chain-head, root-prepare and post-pair digests; exact token/local-prepare/gateway-decision/root-CAS crash tests prove pre-decision abort versus post-decision roll-forward with no digest cycle or dual writer. |
| Handoff trust and crypto | The signed purpose/role/provider/root-bound trust registry, provenance, validity/rotation/revocation, retained destination private keys, X25519 possession, exact HKDF/XChaCha/AAD contract, signed key envelopes and known-answer/mutation tests pass. |
| Handoff content and staging | Public SHA-256 and single/multi-source lineage-HMAC packages verify independently after signed-envelope opening; every part has one immutable object, multiple protected entries remain inline, and separate mutable staging/import records cannot change manifest bytes. |
| Migration/rollback | Legacy Container and devcontainer data move through the canonical destination-bound replay-safe bundle with closed ownership: generic operation/idempotency/tombstone records exist only in identityLifecycleEvents; namespace and logging parts admit only their explicit terminal domain evidence and no live handle; other domain parts do not duplicate generic state; collision-aware commit plus abort/recovery are rehearsed; dirty sources are excluded. |
| Security/faults | Boundary and phase-injection matrix leaves no escape, secret leak, orphan, corrupt state, duplicate event, or false success. |
| Coverage | The machine-checked parity manifest accounts for every STATUS partial/blocker; no unexplained marker, contradictory aggregate, or unevidenced platform exclusion remains. |
| Performance | Matched release-build median/P95 is comparable to or better than Docker Compose in each metric's declared direction outside the noise band for every affected maintained lane. |
| Publication | Docs, status, schemas, stack pins, compatibility manifest, tests, packages, and exact final heads with GitHub-recorded authorities agree. |
- Docker Engine API
- Docker Compose 5.3.1 source
- Moby 29.2.1 source used for Engine behaviour
- Docker Model Runner
- Socktainer documentation
- Socktainer 1.1.1 source and API/socket contract
- Pinned Containerization LinuxPod
- Runtime capability negotiation
- Remaining macOS parity closure design
- Current stack pins
- Current parity ledger